Merge branch 'home-page-create-plan-modal'
This commit is contained in:
commit
ceb956739b
12 changed files with 794 additions and 1 deletions
93
app/Plan/JsonPlanRepository.php
Normal file
93
app/Plan/JsonPlanRepository.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Plan;
|
||||||
|
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use App\ValueObjects\EmailAddress;
|
||||||
|
|
||||||
|
class JsonPlanRepository implements PlanRepository
|
||||||
|
{
|
||||||
|
private string $filePath;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
) {
|
||||||
|
$this->filePath = __DIR__ . '/../../data/plans.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(CreatePlanDto $dto): Plan
|
||||||
|
{
|
||||||
|
$plans = $this->readPlans();
|
||||||
|
$id = $this->getNextId($plans);
|
||||||
|
|
||||||
|
$plans[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $dto->name,
|
||||||
|
'userId' => $dto->user->getId(),
|
||||||
|
];
|
||||||
|
$this->writePlans($plans);
|
||||||
|
|
||||||
|
return new Plan(
|
||||||
|
id: $id,
|
||||||
|
name: $dto->name,
|
||||||
|
user: $dto->user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int $id): ?Plan
|
||||||
|
{
|
||||||
|
$plans = $this->readPlans();
|
||||||
|
|
||||||
|
foreach ($plans as $data) {
|
||||||
|
if ($data['id'] === $id) {
|
||||||
|
$user = $this->userRepository->find($data['userId']);
|
||||||
|
if ($user === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Plan(
|
||||||
|
id: $data['id'],
|
||||||
|
name: $data['name'],
|
||||||
|
user: $user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readPlans(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($this->filePath);
|
||||||
|
|
||||||
|
return json_decode($content, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writePlans(array $plans): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$this->filePath,
|
||||||
|
json_encode($plans, JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNextId(array $plans): int
|
||||||
|
{
|
||||||
|
if (empty($plans)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxId = -1;
|
||||||
|
foreach ($plans as $plan) {
|
||||||
|
if ($plan['id'] > $maxId) {
|
||||||
|
$maxId = $plan['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxId + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Plan/PlanController.php
Normal file
56
app/Plan/PlanController.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Plan;
|
||||||
|
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Plan\UseCases\CreatePlan;
|
||||||
|
use App\Plan\UseCases\CreatePlanRequest;
|
||||||
|
use DomainException;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
|
class PlanController
|
||||||
|
{
|
||||||
|
public function createPlan(
|
||||||
|
Request $request,
|
||||||
|
Response $response,
|
||||||
|
CreatePlan $createPlanUseCase,
|
||||||
|
): Response {
|
||||||
|
$data = json_decode((string) $request->getBody(), true) ?? [];
|
||||||
|
|
||||||
|
$userId = isset($data['userId']) ? (int) $data['userId'] : null;
|
||||||
|
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
|
||||||
|
$name = $data['name'] ?? null;
|
||||||
|
$dateStart = $data['dateStart'] ?? null;
|
||||||
|
$dateEnd = $data['dateEnd'] ?? null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$plan = $createPlanUseCase->execute(new CreatePlanRequest(
|
||||||
|
userId: $userId,
|
||||||
|
textId: $textId,
|
||||||
|
name: $name,
|
||||||
|
dateStart: $dateStart,
|
||||||
|
dateEnd: $dateEnd,
|
||||||
|
));
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
$response->getBody()->write(
|
||||||
|
json_encode(['error' => $exception->getMessage()])
|
||||||
|
);
|
||||||
|
return $response->withStatus(400)
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (DomainException $exception) {
|
||||||
|
$response->getBody()->write(
|
||||||
|
json_encode(['error' => $exception->getMessage()])
|
||||||
|
);
|
||||||
|
return $response->withStatus(404)
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode([
|
||||||
|
'id' => $plan->getId(),
|
||||||
|
'name' => $plan->getName(),
|
||||||
|
]));
|
||||||
|
return $response->withStatus(201)
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/ScheduledNode/JsonScheduledNodeRepository.php
Normal file
67
app/ScheduledNode/JsonScheduledNodeRepository.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\ScheduledNode;
|
||||||
|
|
||||||
|
class JsonScheduledNodeRepository implements ScheduledNodeRepository
|
||||||
|
{
|
||||||
|
private string $filePath;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->filePath = __DIR__ . '/../../data/scheduledNodes.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(CreateScheduledNodeDto $dto): ScheduledNode
|
||||||
|
{
|
||||||
|
$scheduledNodes = $this->readScheduledNodes();
|
||||||
|
$id = $this->getNextId($scheduledNodes);
|
||||||
|
|
||||||
|
$scheduledNodes[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'date' => $dto->date->format('Y-m-d'),
|
||||||
|
'planId' => $dto->plan->getId(),
|
||||||
|
];
|
||||||
|
$this->writeScheduledNodes($scheduledNodes);
|
||||||
|
|
||||||
|
return new ScheduledNode(
|
||||||
|
id: $id,
|
||||||
|
date: $dto->date,
|
||||||
|
plan: $dto->plan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readScheduledNodes(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($this->filePath);
|
||||||
|
|
||||||
|
return json_decode($content, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeScheduledNodes(array $scheduledNodes): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$this->filePath,
|
||||||
|
json_encode($scheduledNodes, JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNextId(array $scheduledNodes): int
|
||||||
|
{
|
||||||
|
if (empty($scheduledNodes)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxId = -1;
|
||||||
|
foreach ($scheduledNodes as $scheduledNode) {
|
||||||
|
if ($scheduledNode['id'] > $maxId) {
|
||||||
|
$maxId = $scheduledNode['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxId + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
app/User/JsonUserRepository.php
Normal file
84
app/User/JsonUserRepository.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User;
|
||||||
|
|
||||||
|
use App\User\UseCases\CreateUserDto;
|
||||||
|
use App\ValueObjects\EmailAddress;
|
||||||
|
|
||||||
|
class JsonUserRepository implements UserRepository
|
||||||
|
{
|
||||||
|
private string $filePath;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->filePath = __DIR__ . '/../../data/users.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(CreateUserDto $dto): User
|
||||||
|
{
|
||||||
|
$users = $this->readUsers();
|
||||||
|
$id = $this->getNextId($users);
|
||||||
|
|
||||||
|
$users[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'email' => (string) $dto->email,
|
||||||
|
];
|
||||||
|
$this->writeUsers($users);
|
||||||
|
|
||||||
|
return new User(
|
||||||
|
id: $id,
|
||||||
|
email: $dto->email,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int $id): ?User
|
||||||
|
{
|
||||||
|
$users = $this->readUsers();
|
||||||
|
|
||||||
|
foreach ($users as $data) {
|
||||||
|
if ($data['id'] === $id) {
|
||||||
|
return new User(
|
||||||
|
id: $data['id'],
|
||||||
|
email: new EmailAddress($data['email']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readUsers(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($this->filePath);
|
||||||
|
|
||||||
|
return json_decode($content, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeUsers(array $users): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$this->filePath,
|
||||||
|
json_encode($users, JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNextId(array $users): int
|
||||||
|
{
|
||||||
|
if (empty($users)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxId = -1;
|
||||||
|
foreach ($users as $user) {
|
||||||
|
if ($user['id'] > $maxId) {
|
||||||
|
$maxId = $user['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxId + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ use DI\Bridge\Slim\Bridge;
|
||||||
use App\View\ViewController;
|
use App\View\ViewController;
|
||||||
use App\Text\TextController;
|
use App\Text\TextController;
|
||||||
use App\Node\NodeController;
|
use App\Node\NodeController;
|
||||||
|
use App\Plan\PlanController;
|
||||||
|
|
||||||
$container = require __DIR__ . '/container.php';
|
$container = require __DIR__ . '/container.php';
|
||||||
$app = Bridge::create($container);
|
$app = Bridge::create($container);
|
||||||
|
|
@ -26,4 +27,6 @@ $app->get('/api/nodes/{textId}', [NodeController::class, 'getNodesOfText']);
|
||||||
$app->post('/api/nodes/bulk', [NodeController::class, 'bulkCreateNodes']);
|
$app->post('/api/nodes/bulk', [NodeController::class, 'bulkCreateNodes']);
|
||||||
$app->post('/api/nodes', [NodeController::class, 'createNode']);
|
$app->post('/api/nodes', [NodeController::class, 'createNode']);
|
||||||
|
|
||||||
|
$app->post('/api/plans', [PlanController::class, 'createPlan']);
|
||||||
|
|
||||||
return $app;
|
return $app;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,20 @@ use App\Text\TextRepository;
|
||||||
use App\Text\JsonTextRepository;
|
use App\Text\JsonTextRepository;
|
||||||
use App\Node\NodeRepository;
|
use App\Node\NodeRepository;
|
||||||
use App\Node\JsonNodeRepository;
|
use App\Node\JsonNodeRepository;
|
||||||
|
use App\Plan\PlanRepository;
|
||||||
|
use App\Plan\JsonPlanRepository;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use App\User\JsonUserRepository;
|
||||||
|
use App\ScheduledNode\ScheduledNodeRepository;
|
||||||
|
use App\ScheduledNode\JsonScheduledNodeRepository;
|
||||||
|
|
||||||
$container = new Container([
|
$container = new Container([
|
||||||
TextRepository::class => DI\autowire(JsonTextRepository::class),
|
TextRepository::class => DI\autowire(JsonTextRepository::class),
|
||||||
NodeRepository::class => DI\autowire(JsonNodeRepository::class),
|
NodeRepository::class => DI\autowire(JsonNodeRepository::class),
|
||||||
|
PlanRepository::class => DI\autowire(JsonPlanRepository::class),
|
||||||
|
UserRepository::class => DI\autowire(JsonUserRepository::class),
|
||||||
|
ScheduledNodeRepository::class =>
|
||||||
|
DI\autowire(JsonScheduledNodeRepository::class),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $container;
|
return $container;
|
||||||
|
|
|
||||||
98
cypress/e2e/homeCreatePlan.cy.js
Normal file
98
cypress/e2e/homeCreatePlan.cy.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
describe('Create plan modal on the home page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.exec('npm run db:seed')
|
||||||
|
cy.intercept('GET', '/api/texts').as('getTexts')
|
||||||
|
cy.visit('/home')
|
||||||
|
cy.wait('@getTexts')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.exec('npm run db:wipe')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a "Create plan" button on each text', () => {
|
||||||
|
cy.get('#texts-list li').each((textItem) => {
|
||||||
|
cy.wrap(textItem).find('button.create-plan').should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the create plan modal by default', () => {
|
||||||
|
cy.get('#create-plan-modal').should('not.be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the modal when clicking "Create plan"', () => {
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('modal contains name, date start, date end, save, cancel', () => {
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal input.plan-name').should('be.visible')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-start')
|
||||||
|
.should('be.visible')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-end')
|
||||||
|
.should('be.visible')
|
||||||
|
cy.get('#create-plan-modal button.save-plan').should('be.visible')
|
||||||
|
cy.get('#create-plan-modal button.cancel-plan').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the modal when clicking "Cancel"', () => {
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal').should('be.visible')
|
||||||
|
cy.get('#create-plan-modal button.cancel-plan').click()
|
||||||
|
cy.get('#create-plan-modal').should('not.be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits plan details to /api/plans', () => {
|
||||||
|
cy.intercept('POST', '/api/plans').as('createPlan')
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal input.plan-name')
|
||||||
|
.type('My reading plan')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-start')
|
||||||
|
.type('2025-01-01')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-end')
|
||||||
|
.type('2025-01-31')
|
||||||
|
cy.get('#create-plan-modal button.save-plan').click()
|
||||||
|
cy.wait('@createPlan').then((createPlanRequest) => {
|
||||||
|
expect(createPlanRequest.response.statusCode).to.eq(201)
|
||||||
|
expect(createPlanRequest.request.body).to.deep.equal({
|
||||||
|
userId: 0,
|
||||||
|
textId: 0,
|
||||||
|
name: 'My reading plan',
|
||||||
|
dateStart: '2025-01-01',
|
||||||
|
dateEnd: '2025-01-31',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes the modal after successful submit', () => {
|
||||||
|
cy.intercept('POST', '/api/plans').as('createPlan')
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal input.plan-name')
|
||||||
|
.type('Another plan')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-start')
|
||||||
|
.type('2025-02-01')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-end')
|
||||||
|
.type('2025-02-28')
|
||||||
|
cy.get('#create-plan-modal button.save-plan').click()
|
||||||
|
cy.wait('@createPlan')
|
||||||
|
cy.get('#create-plan-modal').should('not.be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not submit if name is empty', () => {
|
||||||
|
cy.intercept('POST', '/api/plans').as('createPlan')
|
||||||
|
cy.get('#texts-list li').first()
|
||||||
|
.find('button.create-plan').click()
|
||||||
|
cy.get('#create-plan-modal input.plan-date-start')
|
||||||
|
.type('2025-01-01')
|
||||||
|
cy.get('#create-plan-modal input.plan-date-end')
|
||||||
|
.type('2025-01-31')
|
||||||
|
cy.get('#create-plan-modal button.save-plan').click()
|
||||||
|
cy.get('@createPlan.all').should('have.length', 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -28,9 +28,22 @@ $nodes = [
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$users = [
|
||||||
|
[
|
||||||
|
'id' => 0,
|
||||||
|
'email' => 'user@example.com',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$plans = [];
|
||||||
|
$scheduledNodes = [];
|
||||||
|
|
||||||
$fileDataMap = [
|
$fileDataMap = [
|
||||||
'texts.json' => $texts,
|
'texts.json' => $texts,
|
||||||
'nodes.json' => $nodes,
|
'nodes.json' => $nodes,
|
||||||
|
'users.json' => $users,
|
||||||
|
'plans.json' => $plans,
|
||||||
|
'scheduledNodes.json' => $scheduledNodes,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($fileDataMap as $file => $data) {
|
foreach ($fileDataMap as $file => $data) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
$files = [
|
$files = [
|
||||||
'texts.json',
|
'texts.json',
|
||||||
'nodes.json',
|
'nodes.json',
|
||||||
|
'users.json',
|
||||||
|
'plans.json',
|
||||||
|
'scheduledNodes.json',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,85 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const textsList = document.getElementById('texts-list');
|
const textsList = document.getElementById('texts-list');
|
||||||
|
const createPlanModal = document.getElementById('create-plan-modal');
|
||||||
|
|
||||||
async function loadTexts() {
|
async function loadTexts() {
|
||||||
const response = await fetch('/api/texts');
|
const response = await fetch('/api/texts');
|
||||||
const texts = await response.json();
|
const texts = await response.json();
|
||||||
textsList.innerHTML = texts
|
textsList.innerHTML = texts
|
||||||
.map(text => '<li>' + text.name + '</li>')
|
.map(text =>
|
||||||
|
'<li>' + text.name +
|
||||||
|
' <button class="create-plan" data-text-id="' +
|
||||||
|
text.id + '">Create plan</button></li>'
|
||||||
|
)
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelPlanButton = createPlanModal.querySelector(
|
||||||
|
'button.cancel-plan'
|
||||||
|
);
|
||||||
|
const savePlanButton = createPlanModal.querySelector(
|
||||||
|
'button.save-plan'
|
||||||
|
);
|
||||||
|
const planNameInput = createPlanModal.querySelector('input.plan-name');
|
||||||
|
const planDateStartInput = createPlanModal.querySelector(
|
||||||
|
'input.plan-date-start'
|
||||||
|
);
|
||||||
|
const planDateEndInput = createPlanModal.querySelector(
|
||||||
|
'input.plan-date-end'
|
||||||
|
);
|
||||||
|
|
||||||
|
function openCreatePlanModal(textId) {
|
||||||
|
createPlanModal.dataset.textId = textId;
|
||||||
|
createPlanModal.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreatePlanModal() {
|
||||||
|
createPlanModal.hidden = true;
|
||||||
|
planNameInput.value = '';
|
||||||
|
planDateStartInput.value = '';
|
||||||
|
planDateEndInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
textsList.addEventListener('click', (clickEvent) => {
|
||||||
|
const createPlanButton = clickEvent.target.closest(
|
||||||
|
'button.create-plan'
|
||||||
|
);
|
||||||
|
if (createPlanButton === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openCreatePlanModal(createPlanButton.dataset.textId);
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelPlanButton.addEventListener('click', () => {
|
||||||
|
closeCreatePlanModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
savePlanButton.addEventListener('click', async () => {
|
||||||
|
const planName = planNameInput.value;
|
||||||
|
const dateStart = planDateStartInput.value;
|
||||||
|
const dateEnd = planDateEndInput.value;
|
||||||
|
const textId = Number(createPlanModal.dataset.textId);
|
||||||
|
|
||||||
|
if (planName === '' || dateStart === '' || dateEnd === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/plans', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: 0,
|
||||||
|
textId: textId,
|
||||||
|
name: planName,
|
||||||
|
dateStart: dateStart,
|
||||||
|
dateEnd: dateEnd,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeCreatePlanModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loadTexts();
|
loadTexts();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
280
tests/e2e/Controllers/PlanControllerTest.php
Normal file
280
tests/e2e/Controllers/PlanControllerTest.php
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\e2e\Controllers;
|
||||||
|
|
||||||
|
use App\Node\CreateNodeDto;
|
||||||
|
use App\Plan\PlanController;
|
||||||
|
use App\Plan\UseCases\CreatePlan;
|
||||||
|
use App\ScheduledNode\UseCases\CreateScheduledNode;
|
||||||
|
use App\Text\CreateTextDto;
|
||||||
|
use App\User\UseCases\CreateUserDto;
|
||||||
|
use App\ValueObjects\EmailAddress;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||||
|
use Slim\Psr7\Factory\StreamFactory;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use Tests\Fakes\FakeNodeRepository;
|
||||||
|
use Tests\Fakes\FakePlanRepository;
|
||||||
|
use Tests\Fakes\FakeScheduledNodeRepository;
|
||||||
|
use Tests\Fakes\FakeTextRepository;
|
||||||
|
use Tests\Fakes\FakeUserRepository;
|
||||||
|
|
||||||
|
class PlanControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private FakePlanRepository $planRepo;
|
||||||
|
private FakeUserRepository $userRepo;
|
||||||
|
private FakeTextRepository $textRepo;
|
||||||
|
private FakeNodeRepository $nodeRepo;
|
||||||
|
private FakeScheduledNodeRepository $scheduledNodeRepo;
|
||||||
|
private CreatePlan $createPlan;
|
||||||
|
private PlanController $controller;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->planRepo = new FakePlanRepository();
|
||||||
|
$this->userRepo = new FakeUserRepository();
|
||||||
|
$this->textRepo = new FakeTextRepository();
|
||||||
|
$this->nodeRepo = new FakeNodeRepository();
|
||||||
|
$this->scheduledNodeRepo = new FakeScheduledNodeRepository();
|
||||||
|
|
||||||
|
$this->userRepo->create(new CreateUserDto(
|
||||||
|
email: new EmailAddress('test@test.com'),
|
||||||
|
));
|
||||||
|
$text = $this->textRepo->create(new CreateTextDto('testname'));
|
||||||
|
$this->nodeRepo->create(new CreateNodeDto(
|
||||||
|
text: $text,
|
||||||
|
title: 'Root Node',
|
||||||
|
parentNode: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$createScheduledNode = new CreateScheduledNode(
|
||||||
|
scheduledNodeRepo: $this->scheduledNodeRepo,
|
||||||
|
planRepo: $this->planRepo,
|
||||||
|
);
|
||||||
|
$this->createPlan = new CreatePlan(
|
||||||
|
$this->planRepo,
|
||||||
|
$this->userRepo,
|
||||||
|
$this->textRepo,
|
||||||
|
$this->nodeRepo,
|
||||||
|
$createScheduledNode,
|
||||||
|
);
|
||||||
|
$this->controller = new PlanController();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeRequest(array $data): ServerRequestInterface
|
||||||
|
{
|
||||||
|
$body = new StreamFactory()->createStream(json_encode($data));
|
||||||
|
return new ServerRequestFactory()
|
||||||
|
->createServerRequest('POST', 'http://localhost/api/plans')
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody($body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_201_with_id_and_name(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('id', $body);
|
||||||
|
$this->assertEquals('My Plan', $body['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_user_id_missing(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_text_id_missing(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_name_missing(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_date_start_missing(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_date_end_missing(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_400_when_date_end_before_start(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-02',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_404_when_user_not_found(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 99,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_returns_404_when_text_not_found(): void
|
||||||
|
{
|
||||||
|
$response = $this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 99,
|
||||||
|
'name' => 'My Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getBody(), true);
|
||||||
|
$this->assertArrayHasKey('error', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_persists_plan_in_repository(): void
|
||||||
|
{
|
||||||
|
$this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'Persistent Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$storedPlan = $this->planRepo->find(0);
|
||||||
|
$this->assertNotNull($storedPlan);
|
||||||
|
$this->assertEquals('Persistent Plan', $storedPlan->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_plan_schedules_nodes(): void
|
||||||
|
{
|
||||||
|
$this->controller->createPlan(
|
||||||
|
$this->makeRequest([
|
||||||
|
'userId' => 0,
|
||||||
|
'textId' => 0,
|
||||||
|
'name' => 'Scheduling Plan',
|
||||||
|
'dateStart' => '2025-01-01',
|
||||||
|
'dateEnd' => '2025-01-01',
|
||||||
|
]),
|
||||||
|
new Response(),
|
||||||
|
$this->createPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
1,
|
||||||
|
$this->scheduledNodeRepo->getNumberOfTimesCreateCalled()
|
||||||
|
);
|
||||||
|
$this->assertNotNull($this->scheduledNodeRepo->find(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,20 @@
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<ul id="texts-list">
|
<ul id="texts-list">
|
||||||
</ul>
|
</ul>
|
||||||
|
<div id="create-plan-modal" hidden>
|
||||||
|
<h2>Create plan</h2>
|
||||||
|
<label>Name
|
||||||
|
<input class="plan-name" type="text" />
|
||||||
|
</label>
|
||||||
|
<label>Start date
|
||||||
|
<input class="plan-date-start" type="date" />
|
||||||
|
</label>
|
||||||
|
<label>End date
|
||||||
|
<input class="plan-date-end" type="date" />
|
||||||
|
</label>
|
||||||
|
<button class="save-plan">Save</button>
|
||||||
|
<button class="cancel-plan">Cancel</button>
|
||||||
|
</div>
|
||||||
<script src="/js/home.js"></script>
|
<script src="/js/home.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue