mirror of
https://github.com/werkraum-media/watchlist.git
synced 2024-11-23 15:36:10 +01:00
[FEATURE] Add JavaScript functionality (#2)
This commit is contained in:
parent
916b2723d5
commit
d38af239ee
26 changed files with 842 additions and 116 deletions
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Tests export-ignore
|
||||||
|
patches export-ignore
|
||||||
|
.github export-ignore
|
||||||
|
|
||||||
|
.gitattributes export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
|
||||||
|
.php-cs-fixer.dist.php export-ignore
|
||||||
|
phpstan.neon export-ignore
|
||||||
|
phpunit.xml.dist export-ignore
|
||||||
|
codeception.dist.yml export-ignore
|
||||||
|
|
||||||
|
shell.nix export-ignore
|
106
.github/workflows/ci.yaml
vendored
106
.github/workflows/ci.yaml
vendored
|
@ -1,23 +1,20 @@
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-composer:
|
check-composer:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Composer
|
- uses: cachix/install-nix-action@v17
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: 8.1
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
env:
|
|
||||||
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Validate composer.json
|
- name: Validate composer.json
|
||||||
run: composer validate
|
run: nix-shell --pure --run project-validate-composer
|
||||||
|
|
||||||
php-linting:
|
php-linting:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -31,62 +28,42 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "${{ matrix.php-version }}"
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
- name: PHP lint
|
- name: PHP lint
|
||||||
run: "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l"
|
run: "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l"
|
||||||
|
|
||||||
xml-linting:
|
xml-linting:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [check-composer]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install PHP
|
- uses: cachix/install-nix-action@v17
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: "8.1"
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
|
|
||||||
- name: Install xmllint
|
- name: Validate XML
|
||||||
run: sudo apt-get install libxml2-utils
|
run: nix-shell --pure --run project-validate-xml
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --prefer-dist --no-progress --no-suggest
|
|
||||||
|
|
||||||
- name: PHPUnit configuration file
|
|
||||||
run: xmllint --schema vendor/phpunit/phpunit/phpunit.xsd --noout phpunit.xml.dist
|
|
||||||
|
|
||||||
- name: Fetch schema for xliff
|
|
||||||
run: wget https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=xliff-core-1.2-strict.xsd
|
|
||||||
|
|
||||||
- name: TYPO3 language files
|
|
||||||
run: xmllint --schema xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf')
|
|
||||||
|
|
||||||
coding-guideline:
|
coding-guideline:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
|
||||||
- php-linting
|
|
||||||
- xml-linting
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install PHP
|
- uses: cachix/install-nix-action@v17
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: "8.1"
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Check Coding Guideline
|
||||||
run: composer install --prefer-dist --no-progress --no-suggest
|
run: nix-shell --pure --run project-coding-guideline
|
||||||
|
|
||||||
- name: Coding Guideline
|
|
||||||
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
|
|
||||||
|
|
||||||
code-quality:
|
code-quality:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
|
||||||
- php-linting
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
@ -103,17 +80,16 @@ jobs:
|
||||||
coverage: none
|
coverage: none
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
- name: Install dependencies with expected TYPO3 version
|
- name: Install dependencies
|
||||||
run: composer require --no-interaction --prefer-dist --no-progress
|
run: |-
|
||||||
|
composer require --no-interaction --prefer-dist --no-progress
|
||||||
|
./vendor/bin/codecept build
|
||||||
|
|
||||||
- name: Code Quality (by PHPStan)
|
- name: Code Quality (by PHPStan)
|
||||||
run: ./vendor/bin/phpstan analyse
|
run: ./vendor/bin/phpstan analyse
|
||||||
|
|
||||||
tests-mysql:
|
tests-mysql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
|
||||||
- php-linting
|
|
||||||
- xml-linting
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
@ -137,14 +113,32 @@ jobs:
|
||||||
mysql database: 'typo3'
|
mysql database: 'typo3'
|
||||||
mysql root password: 'root'
|
mysql root password: 'root'
|
||||||
|
|
||||||
|
- name: Wait for MySQL
|
||||||
|
run: |
|
||||||
|
while ! mysqladmin ping --host=127.0.0.1 --password=root --silent; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: composer require --no-interaction --prefer-dist --no-progress
|
run: composer install --no-interaction --prefer-dist --no-progress
|
||||||
|
|
||||||
- name: PHPUnit Tests
|
- name: PHPUnit Tests
|
||||||
run: |-
|
env:
|
||||||
export typo3DatabaseDriver="pdo_mysql"
|
typo3DatabaseDriver: "pdo_mysql"
|
||||||
export typo3DatabaseName="typo3"
|
typo3DatabaseName: "typo3"
|
||||||
export typo3DatabaseHost="127.0.0.1"
|
typo3DatabaseHost: "127.0.0.1"
|
||||||
export typo3DatabaseUsername="root"
|
typo3DatabaseUsername: "root"
|
||||||
export typo3DatabasePassword="root"
|
typo3DatabasePassword: "root"
|
||||||
./vendor/bin/phpunit --testdox
|
run: ./vendor/bin/phpunit --testdox
|
||||||
|
|
||||||
|
tests-acceptance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v17
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
- name: Run Acceptance Tests
|
||||||
|
run: nix-shell --pure --run project-test-acceptance
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
||||||
/composer.lock
|
/composer.lock
|
||||||
/.php-cs-fixer.cache
|
/.php-cs-fixer.cache
|
||||||
|
/.Build/
|
||||||
/vendor/
|
/vendor/
|
||||||
/public/
|
|
||||||
/var/
|
|
||||||
|
|
112
Classes/Middleware/CookieSessionMiddleware.php
Normal file
112
Classes/Middleware/CookieSessionMiddleware.php
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Watchlist\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||||
|
use WerkraumMedia\Watchlist\Session\CookieSessionService;
|
||||||
|
|
||||||
|
class CookieSessionMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
private CookieSessionService $cookieSession;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
CookieSessionService $cookieSession
|
||||||
|
) {
|
||||||
|
$this->cookieSession = $cookieSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
RequestHandlerInterface $handler
|
||||||
|
): ResponseInterface {
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
if ($this->shouldAddCookie($request)) {
|
||||||
|
return $this->addCookie($response, $request);
|
||||||
|
}
|
||||||
|
if ($this->shouldRemoveCookie($request)) {
|
||||||
|
return $this->removeCookie($response, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldAddCookie(ServerRequestInterface $request): bool
|
||||||
|
{
|
||||||
|
return $this->cookieSession->getCookieValue() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addCookie(
|
||||||
|
ResponseInterface $response,
|
||||||
|
ServerRequestInterface $request
|
||||||
|
): ResponseInterface {
|
||||||
|
return $response->withAddedHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
$this->getCookie($request)->__toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldRemoveCookie(ServerRequestInterface $request): bool
|
||||||
|
{
|
||||||
|
$cookieName = $this->cookieSession->getCookieName();
|
||||||
|
|
||||||
|
return $this->cookieSession->getCookieValue() === ''
|
||||||
|
&& isset($request->getCookieParams()[$cookieName])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeCookie(
|
||||||
|
ResponseInterface $response,
|
||||||
|
ServerRequestInterface $request
|
||||||
|
): ResponseInterface {
|
||||||
|
$cookie = $this->getCookie($request)
|
||||||
|
->withExpires(-1);
|
||||||
|
return $response->withAddedHeader('Set-Cookie', $cookie->__toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCookie(ServerRequestInterface $request): Cookie
|
||||||
|
{
|
||||||
|
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||||
|
if (!$normalizedParams instanceof NormalizedParams) {
|
||||||
|
throw new \Exception('Could not retrieve normalized params from request.', 1664357339);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Cookie(
|
||||||
|
$this->cookieSession->getCookieName(),
|
||||||
|
$this->cookieSession->getCookieValue(),
|
||||||
|
$GLOBALS['EXEC_TIME'] + 7776000, // 90 days
|
||||||
|
$normalizedParams->getSitePath() . TYPO3_mainDir,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Cookie::SAMESITE_STRICT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,25 +23,33 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace WerkraumMedia\Watchlist\Session;
|
namespace WerkraumMedia\Watchlist\Session;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Http\ServerRequest;
|
||||||
use TYPO3\CMS\Extbase\Property\PropertyMapper;
|
use TYPO3\CMS\Extbase\Property\PropertyMapper;
|
||||||
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
|
|
||||||
use WerkraumMedia\Watchlist\Domain\Model\Item;
|
use WerkraumMedia\Watchlist\Domain\Model\Item;
|
||||||
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
|
use WerkraumMedia\Watchlist\Domain\Model\Watchlist;
|
||||||
|
|
||||||
class Typo3FrontendSessionService implements SessionServiceInterface
|
class CookieSessionService implements SessionServiceInterface
|
||||||
{
|
{
|
||||||
private PropertyMapper $propertyMapper;
|
private PropertyMapper $propertyMapper;
|
||||||
|
|
||||||
public function __construct(
|
private array $watchlists = [];
|
||||||
PropertyMapper $propertyMapper
|
|
||||||
) {
|
// Seems to be a bug leading to different instances if we use constructor.
|
||||||
|
public function injectPropertyMapper(PropertyMapper $propertyMapper): void
|
||||||
|
{
|
||||||
$this->propertyMapper = $propertyMapper;
|
$this->propertyMapper = $propertyMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWatchlist(string $identifier): ?Watchlist
|
public function getWatchlist(string $identifier): ?Watchlist
|
||||||
{
|
{
|
||||||
$items = $this->getTsfe()->fe_user->getSessionData('watchlist-' . $identifier) ?: [];
|
$cookieName = $this->getCookieName();
|
||||||
if ($items === [] || is_array($items) === false) {
|
$cookie = $this->getRequest()->getCookieParams()[$cookieName] ?? '';
|
||||||
|
$items = array_filter(explode(
|
||||||
|
',',
|
||||||
|
$this->getRequest()->getCookieParams()['watchlist'] ?? ''
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($items === []) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,13 +69,24 @@ class Typo3FrontendSessionService implements SessionServiceInterface
|
||||||
|
|
||||||
public function update(Watchlist $watchlist): void
|
public function update(Watchlist $watchlist): void
|
||||||
{
|
{
|
||||||
$this->getTsfe()->fe_user->setAndSaveSessionData(
|
$this->watchlists[$watchlist->getIdentifier()] = array_map(
|
||||||
'watchlist-' . $watchlist->getIdentifier(),
|
fn (Item $item) => $item->getUniqueIdentifier(),
|
||||||
array_map(fn (Item $item) => $item->getUniqueIdentifier(), $watchlist->getItems())
|
$watchlist->getItems()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private function getTsfe(): TypoScriptFrontendController
|
|
||||||
|
public function getCookieName(): string
|
||||||
{
|
{
|
||||||
return $GLOBALS['TSFE'];
|
return 'watchlist';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCookieValue(): string
|
||||||
|
{
|
||||||
|
return implode(',', $this->watchlists['default'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRequest(): ServerRequest
|
||||||
|
{
|
||||||
|
return $GLOBALS['TYPO3_REQUEST'];
|
||||||
}
|
}
|
||||||
}
|
}
|
36
Configuration/RequestMiddlewares.php
Normal file
36
Configuration/RequestMiddlewares.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'frontend' => [
|
||||||
|
'watchlist-cookiesession' => [
|
||||||
|
'target' => \WerkraumMedia\Watchlist\Middleware\CookieSessionMiddleware::class,
|
||||||
|
'after' => [
|
||||||
|
'typo3/cms-frontend/content-length-headers',
|
||||||
|
],
|
||||||
|
'before' => [
|
||||||
|
'typo3/cms-frontend/output-compression',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -11,3 +11,7 @@ services:
|
||||||
|
|
||||||
WerkraumMedia\Watchlist\Domain\Items\Page\ItemHandler:
|
WerkraumMedia\Watchlist\Domain\Items\Page\ItemHandler:
|
||||||
tags: ['watchlist.itemHandler']
|
tags: ['watchlist.itemHandler']
|
||||||
|
|
||||||
|
WerkraumMedia\Watchlist\Session\CookieSessionService:
|
||||||
|
# Has state and needs to be shared in order to provide this state to middleware
|
||||||
|
shared: true
|
||||||
|
|
51
README.rst
51
README.rst
|
@ -14,6 +14,7 @@ Feature set
|
||||||
===========
|
===========
|
||||||
|
|
||||||
The extension provides an controller with actions to add and remove items from watchlist.
|
The extension provides an controller with actions to add and remove items from watchlist.
|
||||||
|
Each item can only exist once within the watchlist.
|
||||||
|
|
||||||
There is also an action to show the current watchlist.
|
There is also an action to show the current watchlist.
|
||||||
|
|
||||||
|
@ -21,11 +22,19 @@ The extension ships with support for items of type ``pages``,
|
||||||
but this is more a demonstration an used for testing.
|
but this is more a demonstration an used for testing.
|
||||||
Projects are way too different and should provide their own items, see "Custom Items".
|
Projects are way too different and should provide their own items, see "Custom Items".
|
||||||
|
|
||||||
|
The extension also provides a JavaScript which will parse data-Attributes of DOM and
|
||||||
|
attach listener to add elements to the watchlist.
|
||||||
|
|
||||||
Custom Items
|
Custom Items
|
||||||
============
|
============
|
||||||
|
|
||||||
A developer needs to implement an ``ItemHandler`` and an class representing the ``Item``.
|
A developer needs to implement an ``ItemHandler`` and an class representing the ``Item``.
|
||||||
|
|
||||||
|
The used identifier has to be unique throughout the system. The first part is the
|
||||||
|
item type which is returned by the ItemHandler to be handled, followed by a minus and
|
||||||
|
the rest of the identifier, leading to: ``<type>-rest-of-identifier``.
|
||||||
|
Commas should not be used within the identifier.
|
||||||
|
|
||||||
``ItemHandler``
|
``ItemHandler``
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -51,3 +60,45 @@ The Handler needs to be registered via Symfony Tags, e.g. via ``Services.yaml``:
|
||||||
--------
|
--------
|
||||||
|
|
||||||
The class needs to implement the ``WerkraumMedia\Watchlist\Domain\Model\Item``.
|
The class needs to implement the ``WerkraumMedia\Watchlist\Domain\Model\Item``.
|
||||||
|
|
||||||
|
JavaScript
|
||||||
|
==========
|
||||||
|
|
||||||
|
The JavaScript respects two ``data-`` attributes:
|
||||||
|
|
||||||
|
``data-watchlist-counter``
|
||||||
|
Defines that this element will hold the current number of items on watch list.
|
||||||
|
The JavaScript will update the content of the element.
|
||||||
|
|
||||||
|
.. code:: html
|
||||||
|
|
||||||
|
<span data-watchlist-counter class="watchlist-badge-counter"></span>
|
||||||
|
|
||||||
|
``data-watchlist-item``
|
||||||
|
Defines that this element represents an actual item.
|
||||||
|
The attribute needs the identifier of the item as value, e.g.: ``data-watchlist-item="page-1"``
|
||||||
|
|
||||||
|
An EventListener will be added listening for click events on that element.
|
||||||
|
Each click will toggle the state of the item on the watch list.
|
||||||
|
|
||||||
|
The JavaScript will add a CSS class to the element, depending on the state of the item.
|
||||||
|
``watchlist-inactive`` will be added in case the item is not on the watchlist.
|
||||||
|
``watchlist-active`` will be added in case the item is on the watchlist.
|
||||||
|
|
||||||
|
A custom Event will be triggered whenever an item is added or removed from the watchlist.
|
||||||
|
The event is triggered on the ``document``.
|
||||||
|
The event provides the identifier of the item.
|
||||||
|
An example listener can look like:
|
||||||
|
|
||||||
|
.. code:: js
|
||||||
|
|
||||||
|
document.addEventListener('WatchlistUpdate', function(event) {
|
||||||
|
console.log(event.detail.watchlistItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
A concrete example can be found within ``Tests/Fixtures/FrontendRendering.typoscript``.
|
||||||
|
This example includes the provided JavaScript file as well as some custom CSS and Markup.
|
||||||
|
The content element is not necessary.
|
||||||
|
|
112
Resources/Public/JavaScript/Watchlist.js
Normal file
112
Resources/Public/JavaScript/Watchlist.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
// From: https://plainjs.com/javascript/utilities/set-cookie-get-cookie-and-delete-cookie-5/
|
||||||
|
const cookie = {
|
||||||
|
name: 'watchlist',
|
||||||
|
days: 7,
|
||||||
|
get: function() {
|
||||||
|
let v = document.cookie.match('(^|;) ?' + cookie.name + '=([^;]*)(;|$)');
|
||||||
|
return v ? v[2] : null;
|
||||||
|
},
|
||||||
|
set: function(value) {
|
||||||
|
let d = new Date;
|
||||||
|
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * cookie.days);
|
||||||
|
document.cookie = cookie.name + "=" + value + ";path=/;expires=" + d.toGMTString();
|
||||||
|
},
|
||||||
|
delete: function() {
|
||||||
|
let d = new Date;
|
||||||
|
d.setTime(d.getTime() -1);
|
||||||
|
document.cookie = cookie.name + "=;path=/;expires=" + d.toGMTString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const watchlist = {
|
||||||
|
get: function() {
|
||||||
|
const cookieValue = cookie.get();
|
||||||
|
if (cookieValue === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookieValue.split(',');
|
||||||
|
},
|
||||||
|
save: function(items) {
|
||||||
|
var cookieValue = items.join(',');
|
||||||
|
|
||||||
|
if (cookieValue == '') {
|
||||||
|
cookie.delete();
|
||||||
|
} else {
|
||||||
|
cookie.set(items.join(','));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleItem: function(identifier) {
|
||||||
|
let items = watchlist.get();
|
||||||
|
const position = items.indexOf(identifier);
|
||||||
|
|
||||||
|
if (position === -1) {
|
||||||
|
items = items.concat(identifier);
|
||||||
|
} else {
|
||||||
|
items.splice(position, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchlist.save(items);
|
||||||
|
watchlist.triggerUpdateEvent(identifier);
|
||||||
|
},
|
||||||
|
triggerUpdateEvent: function(identifier) {
|
||||||
|
const event = new CustomEvent('WatchlistUpdate', {
|
||||||
|
detail: {
|
||||||
|
watchlistItem: identifier
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const watchlistHtml = {
|
||||||
|
selectors: {
|
||||||
|
items: '[data-watchlist-item]',
|
||||||
|
counter: '[data-watchlist-counter]'
|
||||||
|
},
|
||||||
|
getItems: function() {
|
||||||
|
return document.querySelectorAll(watchlistHtml.selectors.items);
|
||||||
|
},
|
||||||
|
getCounters: function() {
|
||||||
|
return document.querySelectorAll(watchlistHtml.selectors.counter);
|
||||||
|
},
|
||||||
|
update: function() {
|
||||||
|
watchlistHtml.updateCounter();
|
||||||
|
watchlistHtml.updateItems();
|
||||||
|
},
|
||||||
|
updateCounter: function() {
|
||||||
|
const count = watchlist.get().length;
|
||||||
|
watchlistHtml.getCounters().forEach(function (element) {
|
||||||
|
element.innerText = count;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateItems: function() {
|
||||||
|
const items = watchlist.get();
|
||||||
|
watchlistHtml.getItems().forEach(function (element) {
|
||||||
|
if (items.indexOf(element.dataset.watchlistItem) === -1) {
|
||||||
|
element.classList.add('watchlist-inactive');
|
||||||
|
element.classList.remove('watchlist-active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList.remove('watchlist-inactive');
|
||||||
|
element.classList.add('watchlist-active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
watchlistHtml.update();
|
||||||
|
|
||||||
|
watchlistHtml.getItems().forEach(function (element) {
|
||||||
|
element.addEventListener('click', function(event) {
|
||||||
|
watchlist.toggleItem(event.currentTarget.dataset.watchlistItem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener('WatchlistUpdate', function(event) {
|
||||||
|
watchlistHtml.update();
|
||||||
|
});
|
||||||
|
})();
|
0
Tests/Acceptance/Data/.gitkeep
Normal file
0
Tests/Acceptance/Data/.gitkeep
Normal file
95
Tests/Acceptance/JavaScriptCest.php
Normal file
95
Tests/Acceptance/JavaScriptCest.php
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Watchlist\Tests\Acceptance;
|
||||||
|
|
||||||
|
use WerkraumMedia\Watchlist\Tests\Acceptance\Support\AcceptanceTester;
|
||||||
|
|
||||||
|
class JavaScriptCest
|
||||||
|
{
|
||||||
|
private const SELECTOR_COUNTER = '.watchlist-badge-counter';
|
||||||
|
|
||||||
|
public function counterShowsZeroByDefault(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/');
|
||||||
|
$I->see('Watchlist is empty');
|
||||||
|
$I->see('0', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canAddItem(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$I->amOnPage('/');
|
||||||
|
$I->see('Watchlist is empty');
|
||||||
|
|
||||||
|
$I->see('Page 1 zur Merkliste hinzufügen');
|
||||||
|
$I->dontSee('Page 1 von Merkliste entfernen');
|
||||||
|
$I->click('button[data-watchlist-item="page-1"]');
|
||||||
|
|
||||||
|
$I->dontSee('Page 1 zur Merkliste hinzufügen');
|
||||||
|
$I->see('Page 1 von Merkliste entfernen');
|
||||||
|
$I->see('1', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canRemoveItem(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$this->canAddItem($I);
|
||||||
|
|
||||||
|
$I->click('button[data-watchlist-item="page-1"]');
|
||||||
|
|
||||||
|
$I->see('Page 1 zur Merkliste hinzufügen');
|
||||||
|
$I->dontSee('Page 1 von Merkliste entfernen');
|
||||||
|
$I->see('0', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canAddTwoItems(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$this->canAddItem($I);
|
||||||
|
|
||||||
|
$I->click('button[data-watchlist-item="page-2"]');
|
||||||
|
$I->dontSee('Page 2 zur Merkliste hinzufügen');
|
||||||
|
$I->see('Page 2 von Merkliste entfernen');
|
||||||
|
$I->see('2', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canRemoveFirstItem(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$this->canAddTwoItems($I);
|
||||||
|
|
||||||
|
$I->click('button[data-watchlist-item="page-1"]');
|
||||||
|
$I->see('Page 1 zur Merkliste hinzufügen');
|
||||||
|
$I->see('Page 2 von Merkliste entfernen');
|
||||||
|
$I->dontSee('Page 2 zur Merkliste hinzufügen');
|
||||||
|
$I->see('1', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function keepsStateOnReload(AcceptanceTester $I): void
|
||||||
|
{
|
||||||
|
$this->canAddItem($I);
|
||||||
|
|
||||||
|
$I->amOnPage('/');
|
||||||
|
|
||||||
|
$I->dontSee('Page 1 zur Merkliste hinzufügen');
|
||||||
|
$I->see('Page 1 von Merkliste entfernen');
|
||||||
|
$I->see('1', self::SELECTOR_COUNTER);
|
||||||
|
}
|
||||||
|
}
|
51
Tests/Acceptance/Support/AcceptanceTester.php
Normal file
51
Tests/Acceptance/Support/AcceptanceTester.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Watchlist\Tests\Acceptance\Support;
|
||||||
|
|
||||||
|
use Codeception\Actor;
|
||||||
|
use WerkraumMedia\Watchlist\Tests\Acceptance\Support\_generated\AcceptanceTesterActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherited Methods
|
||||||
|
* @method void wantToTest($text)
|
||||||
|
* @method void wantTo($text)
|
||||||
|
* @method void execute($callable)
|
||||||
|
* @method void expectTo($prediction)
|
||||||
|
* @method void expect($prediction)
|
||||||
|
* @method void amGoingTo($argumentation)
|
||||||
|
* @method void am($role)
|
||||||
|
* @method void lookForwardTo($achieveValue)
|
||||||
|
* @method void comment($description)
|
||||||
|
* @method void pause()
|
||||||
|
*
|
||||||
|
* @SuppressWarnings(PHPMD)
|
||||||
|
*/
|
||||||
|
class AcceptanceTester extends Actor
|
||||||
|
{
|
||||||
|
use AcceptanceTesterActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define custom actions here
|
||||||
|
*/
|
||||||
|
}
|
61
Tests/Acceptance/Support/Environment.php
Normal file
61
Tests/Acceptance/Support/Environment.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Watchlist\Tests\Acceptance\Support;
|
||||||
|
|
||||||
|
use TYPO3\TestingFramework\Core\Acceptance\Extension\BackendEnvironment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load various core extensions and styleguide and call styleguide generator
|
||||||
|
*/
|
||||||
|
class Environment extends BackendEnvironment
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Load a list of core extensions and styleguide
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $localConfig = [
|
||||||
|
'coreExtensionsToLoad' => [
|
||||||
|
'install',
|
||||||
|
'core',
|
||||||
|
'backend',
|
||||||
|
'extbase',
|
||||||
|
'frontend',
|
||||||
|
'fluid',
|
||||||
|
'fluid_styled_content',
|
||||||
|
],
|
||||||
|
'testExtensionsToLoad' => [
|
||||||
|
'typo3conf/ext/watchlist',
|
||||||
|
],
|
||||||
|
'csvDatabaseFixtures' => [
|
||||||
|
__DIR__ . '/../../Fixtures/BasicDatabase.csv',
|
||||||
|
],
|
||||||
|
'additionalFoldersToCreate' => [
|
||||||
|
'config',
|
||||||
|
],
|
||||||
|
'pathsToLinkInTestInstance' => [
|
||||||
|
'typo3conf/ext/watchlist/Tests/Fixtures/Sites/' => 'config/sites',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
2
Tests/Acceptance/Support/_generated/.gitignore
vendored
Normal file
2
Tests/Acceptance/Support/_generated/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
Tests/Acceptance/_output/.gitignore
vendored
Normal file
2
Tests/Acceptance/_output/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
8
Tests/Fixtures/BasicDatabase.csv
Normal file
8
Tests/Fixtures/BasicDatabase.csv
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"pages",,,,,,
|
||||||
|
,"uid","pid","slug","title",,
|
||||||
|
,1,0,"/","Page Title",,
|
||||||
|
,2,1,"/page-2","Page 2 Title",,
|
||||||
|
"sys_template",,,,,,
|
||||||
|
,"uid","pid","root","clear","constants","config"
|
||||||
|
,1,1,1,3,"databasePlatform = mysql","<INCLUDE_TYPOSCRIPT: source=""FILE:EXT:watchlist/Tests/Fixtures/FrontendRendering.typoscript"">
|
||||||
|
<INCLUDE_TYPOSCRIPT: source=""FILE:EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript"">"
|
|
35
Tests/Fixtures/FrontendRendering.typoscript
Normal file
35
Tests/Fixtures/FrontendRendering.typoscript
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
page = PAGE
|
||||||
|
page {
|
||||||
|
# Include JavaScript in order to test the JavaScript
|
||||||
|
includeJSFooter.watchlist = EXT:watchlist/Resources/Public/JavaScript/Watchlist.js
|
||||||
|
|
||||||
|
# Render the content element
|
||||||
|
10 =< tt_content.watchlist_watchlist.20
|
||||||
|
|
||||||
|
# Add minimum CSS for checking functionality within Acceptance tests
|
||||||
|
cssInline {
|
||||||
|
10 = TEXT
|
||||||
|
10.value (
|
||||||
|
.watchlist-inactive .remove-from-list { display: none; }
|
||||||
|
.watchlist-active .add-to-list { display: none; }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add some example markup for Acceptance Tests
|
||||||
|
20 = TEXT
|
||||||
|
20.value (
|
||||||
|
<span data-watchlist-counter class="watchlist-badge-counter"></span>
|
||||||
|
|
||||||
|
<button data-watchlist-item="page-1" class="watchlist-btn watchlist-inactive">
|
||||||
|
Page 1
|
||||||
|
<span class="add-to-list">zur Merkliste hinzufügen</span>
|
||||||
|
<span class="remove-from-list">von Merkliste entfernen</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button data-watchlist-item="page-2" class="watchlist-btn watchlist-inactive">
|
||||||
|
Page 2
|
||||||
|
<span class="add-to-list">zur Merkliste hinzufügen</span>
|
||||||
|
<span class="remove-from-list">von Merkliste entfernen</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -21,8 +21,9 @@ declare(strict_types=1);
|
||||||
* 02110-1301, USA.
|
* 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace WerkraumMedia\Watchlist\Tests\Functional\Frontend;
|
namespace WerkraumMedia\Watchlist\Tests\Functional;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
||||||
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
|
||||||
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
@ -40,21 +41,15 @@ class BasicsTest extends FunctionalTestCase
|
||||||
'typo3conf/ext/watchlist',
|
'typo3conf/ext/watchlist',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $pathsToProvideInTestInstance = [
|
protected $pathsToLinkInTestInstance = [
|
||||||
'typo3conf/ext/watchlist/Tests/Functional/Frontend/Fixtures/Sites/' => 'typo3conf/sites/',
|
'typo3conf/ext/watchlist/Tests/Fixtures/Sites' => 'typo3conf/sites',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->setUpBackendUserFromFixture(1);
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/BasicDatabase.csv');
|
||||||
|
|
||||||
$this->importCSVDataSet(__DIR__ . '/Fixtures/BasicDatabase.csv');
|
|
||||||
$this->setUpFrontendRootPage(1, [
|
|
||||||
'EXT:watchlist/Tests/Functional/Frontend/Fixtures/FrontendRendering.typoscript',
|
|
||||||
'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,14 +78,14 @@ class BasicsTest extends FunctionalTestCase
|
||||||
$result = $this->executeFrontendRequest($request);
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
self::assertIsRedirect('http://localhost/?id=1', $result);
|
self::assertIsRedirect('http://localhost/?id=1', $result);
|
||||||
self::assertHasCookie('fe_typo_user', $result);
|
self::assertCookie('page-1', $this->getCookie($result));
|
||||||
|
|
||||||
$request = new InternalRequest();
|
$request = new InternalRequest();
|
||||||
$request = $request->withPageId(1);
|
$request = $request->withPageId(1);
|
||||||
$request = $request->withHeader('Cookie', self::getCookies($result));
|
$request = $request->withHeader('Cookie', 'watchlist=page-1');
|
||||||
$result = $this->executeFrontendRequest($request);
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
self::assertStringContainsString('Page Title', $result->getBody()->__toString());
|
self::assertStringContainsString('<li>Page Title</li>', $result->getBody()->__toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,10 +100,10 @@ class BasicsTest extends FunctionalTestCase
|
||||||
$request = $request->withQueryParameter('tx_watchlist_watchlist[item]', 'page-1');
|
$request = $request->withQueryParameter('tx_watchlist_watchlist[item]', 'page-1');
|
||||||
$result = $this->executeFrontendRequest($request);
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
$cookies = self::getCookies($result);
|
self::assertCookie('page-1', $this->getCookie($result));
|
||||||
|
|
||||||
$request = new InternalRequest();
|
$request = new InternalRequest();
|
||||||
$request = $request->withHeader('Cookie', $cookies);
|
$request = $request->withHeader('Cookie', 'watchlist=page-1');
|
||||||
$request = $request->withPageId(1);
|
$request = $request->withPageId(1);
|
||||||
$request = $request->withQueryParameter('tx_watchlist_watchlist[redirectUri]', $request->getUri()->__toString());
|
$request = $request->withQueryParameter('tx_watchlist_watchlist[redirectUri]', $request->getUri()->__toString());
|
||||||
$request = $request->withQueryParameter('tx_watchlist_watchlist[action]', 'remove');
|
$request = $request->withQueryParameter('tx_watchlist_watchlist[action]', 'remove');
|
||||||
|
@ -116,9 +111,11 @@ class BasicsTest extends FunctionalTestCase
|
||||||
$result = $this->executeFrontendRequest($request);
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
self::assertIsRedirect('http://localhost/?id=1', $result);
|
self::assertIsRedirect('http://localhost/?id=1', $result);
|
||||||
|
$cookie = $this->getCookie($result);
|
||||||
|
self::assertInstanceOf(Cookie::class, $cookie);
|
||||||
|
self::assertLessThan(time(), $cookie->getExpiresTime());
|
||||||
|
|
||||||
$request = new InternalRequest();
|
$request = new InternalRequest();
|
||||||
$request = $request->withHeader('Cookie', $cookies);
|
|
||||||
$request = $request->withPageId(1);
|
$request = $request->withPageId(1);
|
||||||
$result = $this->executeFrontendRequest($request);
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
@ -132,13 +129,24 @@ class BasicsTest extends FunctionalTestCase
|
||||||
self::assertSame($redirectLocation, $result->getHeader('location')[0] ?? '');
|
self::assertSame($redirectLocation, $result->getHeader('location')[0] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function assertHasCookie(string $cookieName, InternalResponse $result): void
|
private static function assertCookie(string $value, ?Cookie $cookie): void
|
||||||
{
|
{
|
||||||
self::assertStringContainsString($cookieName . '=', self::getCookies($result));
|
self::assertInstanceOf(Cookie::class, $cookie);
|
||||||
|
self::assertSame('watchlist', $cookie->getName());
|
||||||
|
self::assertSame('page-1', $cookie->getValue());
|
||||||
|
self::assertNull($cookie->getDomain());
|
||||||
|
self::assertSame('/typo3/', $cookie->getPath());
|
||||||
|
self::assertSame('strict', $cookie->getSameSite());
|
||||||
|
self::assertFalse($cookie->isSecure());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getCookies(InternalResponse $result): string
|
private function getCookie(InternalResponse $result): ?Cookie
|
||||||
{
|
{
|
||||||
return explode(' ', $result->getHeader('Set-Cookie')[0] ?? '', 2)[0] ?? '';
|
$cookie = $result->getHeader('Set-Cookie')[0] ?? '';
|
||||||
|
if ($cookie === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cookie::fromString($cookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
pages
|
|
||||||
,uid,pid,slug,title
|
|
||||||
,1,0,/,Page Title
|
|
|
|
@ -1,2 +0,0 @@
|
||||||
page = PAGE
|
|
||||||
page.10 =< tt_content.watchlist_watchlist.20
|
|
33
codeception.dist.yml
Normal file
33
codeception.dist.yml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
namespace: WerkraumMedia\Watchlist\Tests\Acceptance\Support
|
||||||
|
|
||||||
|
paths:
|
||||||
|
tests: Tests/Acceptance
|
||||||
|
data: Tests/Acceptance/Data
|
||||||
|
output: .Build/web/typo3temp/var/tests/AcceptanceReports
|
||||||
|
support: Tests/Acceptance/Support
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- Codeception\Extension\RunFailed
|
||||||
|
|
||||||
|
suites:
|
||||||
|
acceptance:
|
||||||
|
actor: AcceptanceTester
|
||||||
|
path: .
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- WerkraumMedia\Watchlist\Tests\Acceptance\Support\Environment:
|
||||||
|
typo3DatabaseUsername: 'testing'
|
||||||
|
typo3DatabasePassword: 'testing'
|
||||||
|
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- WebDriver:
|
||||||
|
url: 'http://localhost:8080'
|
||||||
|
browser: 'firefox'
|
||||||
|
restart: true
|
||||||
|
path: ''
|
||||||
|
capabilities:
|
||||||
|
moz:firefoxOptions:
|
||||||
|
args:
|
||||||
|
- '-headless'
|
|
@ -36,6 +36,8 @@
|
||||||
"typo3/cms-frontend": "^11.5"
|
"typo3/cms-frontend": "^11.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"codeception/codeception": "^4.2",
|
||||||
|
"codeception/module-webdriver": "^2.0",
|
||||||
"cweagans/composer-patches": "^1.7",
|
"cweagans/composer-patches": "^1.7",
|
||||||
"friendsofphp/php-cs-fixer": "^3.11",
|
"friendsofphp/php-cs-fixer": "^3.11",
|
||||||
"phpstan/extension-installer": "^1.1",
|
"phpstan/extension-installer": "^1.1",
|
||||||
|
@ -59,22 +61,20 @@
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"typo3/cms": {
|
"typo3/cms": {
|
||||||
"cms-package-dir": "{$vendor-dir}/typo3/cms",
|
"app-dir": ".Build",
|
||||||
"extension-key": "watchlist",
|
"extension-key": "watchlist",
|
||||||
"web-dir": "public"
|
"web-dir": ".Build/web"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"post-autoload-dump": [
|
|
||||||
"@php -r 'is_dir($extFolder=__DIR__.\"/public/typo3conf/ext/\") || mkdir($extFolder, 0777, true);'",
|
|
||||||
"@php -r 'file_exists($extFolder=__DIR__.\"/public/typo3conf/ext/watchlist\") || symlink(__DIR__,$extFolder);'"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extra": {
|
|
||||||
"patches": {
|
"patches": {
|
||||||
"typo3/testing-framework": {
|
"typo3/testing-framework": {
|
||||||
"Support cookies in request": "patches/testing-framework-cookies.patch"
|
"Support cookies in request": "patches/testing-framework-cookies.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"@php -r 'is_dir($extFolder=__DIR__.\"/.Build/web/typo3conf/ext/\") || mkdir($extFolder, 0777, true);'",
|
||||||
|
"@php -r 'file_exists($extFolder=__DIR__.\"/.Build/web/typo3conf/ext/watchlist\") || symlink(__DIR__,$extFolder);'"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ parameters:
|
||||||
- Classes
|
- Classes
|
||||||
- Configuration
|
- Configuration
|
||||||
- Tests
|
- Tests
|
||||||
|
excludePaths:
|
||||||
|
- Tests/Acceptance/Support/_generated/
|
||||||
checkMissingIterableValueType: false
|
checkMissingIterableValueType: false
|
||||||
reportUnmatchedIgnoredErrors: true
|
reportUnmatchedIgnoredErrors: true
|
||||||
checkGenericClassInNonGenericObjectType: false
|
checkGenericClassInNonGenericObjectType: false
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||||
backupGlobals="false"
|
backupGlobals="false"
|
||||||
backupStaticAttributes="false"
|
backupStaticAttributes="false"
|
||||||
beStrictAboutCoversAnnotation="true"
|
|
||||||
bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
|
bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
convertErrorsToExceptions="true"
|
convertErrorsToExceptions="true"
|
||||||
convertWarningsToExceptions="true"
|
convertWarningsToExceptions="true"
|
||||||
forceCoversAnnotation="true"
|
forceCoversAnnotation="false"
|
||||||
processIsolation="false"
|
processIsolation="false"
|
||||||
stopOnError="false"
|
stopOnError="false"
|
||||||
stopOnFailure="false"
|
stopOnFailure="false"
|
||||||
stopOnIncomplete="false"
|
stopOnIncomplete="false"
|
||||||
stopOnSkipped="false"
|
stopOnSkipped="false"
|
||||||
verbose="false"
|
verbose="false"
|
||||||
>
|
>
|
||||||
|
|
||||||
<coverage processUncoveredFiles="true">
|
|
||||||
<include>
|
|
||||||
<directory suffix=".php">Classes</directory>
|
|
||||||
</include>
|
|
||||||
</coverage>
|
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="unit">
|
<testsuite name="unit">
|
||||||
|
@ -32,6 +26,12 @@
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
|
<coverage>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">Classes</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
|
|
||||||
<php>
|
<php>
|
||||||
<env name="typo3DatabaseDriver" value="pdo_sqlite"/>
|
<env name="typo3DatabaseDriver" value="pdo_sqlite"/>
|
||||||
</php>
|
</php>
|
||||||
|
|
94
shell.nix
Normal file
94
shell.nix
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
let
|
||||||
|
projectInstall = pkgs.writeShellApplication {
|
||||||
|
name = "project-install";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.php81
|
||||||
|
pkgs.php81Packages.composer
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
composer install --prefer-dist --no-progress --working-dir="$PROJECT_ROOT"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectValidateComposer = pkgs.writeShellApplication {
|
||||||
|
name = "project-validate-composer";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.php81
|
||||||
|
pkgs.php81Packages.composer
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
composer validate
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectValidateXml = pkgs.writeShellApplication {
|
||||||
|
name = "project-validate-xml";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.libxml2
|
||||||
|
pkgs.wget
|
||||||
|
projectInstall
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
project-install
|
||||||
|
xmllint --schema vendor/phpunit/phpunit/phpunit.xsd --noout phpunit.xml.dist
|
||||||
|
wget --no-check-certificate https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=xliff-core-1.2-strict.xsd
|
||||||
|
xmllint --schema xliff-core-1.2-strict.xsd --noout "$(find Resources -name '*.xlf')"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectCodingGuideline = pkgs.writeShellApplication {
|
||||||
|
name = "project-coding-guideline";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.php81
|
||||||
|
projectInstall
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
project-install
|
||||||
|
./vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectTestAcceptance = pkgs.writeShellApplication {
|
||||||
|
name = "project-test-acceptance";
|
||||||
|
runtimeInputs = [
|
||||||
|
projectInstall
|
||||||
|
pkgs.sqlite
|
||||||
|
pkgs.firefox
|
||||||
|
pkgs.geckodriver
|
||||||
|
pkgs.php81
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
project-install
|
||||||
|
|
||||||
|
_instance_path="$PROJECT_ROOT/.Build/web/typo3temp/var/tests/acceptance/"
|
||||||
|
|
||||||
|
mkdir -p "$_instance_path"
|
||||||
|
|
||||||
|
geckodriver > /dev/null 2>&1 &
|
||||||
|
_pid_geckodriver=$!
|
||||||
|
|
||||||
|
TYPO3_PATH_APP="$_instance_path" \
|
||||||
|
TYPO3_PATH_ROOT="$_instance_path" \
|
||||||
|
php -S 127.0.0.1:8080 -t "$_instance_path" > /dev/null 2>&1 &
|
||||||
|
_pid_php=$!
|
||||||
|
|
||||||
|
./vendor/bin/codecept build
|
||||||
|
./vendor/bin/codecept run
|
||||||
|
|
||||||
|
kill "$_pid_geckodriver" "$_pid_php"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in pkgs.mkShell {
|
||||||
|
name = "TYPO3 Extension Watchlist";
|
||||||
|
buildInputs = [
|
||||||
|
projectValidateComposer
|
||||||
|
projectValidateXml
|
||||||
|
projectCodingGuideline
|
||||||
|
projectTestAcceptance
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PROJECT_ROOT="$(pwd)"
|
||||||
|
|
||||||
|
export typo3DatabaseDriver=pdo_sqlite
|
||||||
|
'';
|
||||||
|
}
|
Loading…
Reference in a new issue