diff --git a/app/Node/NodeController.php b/app/Node/NodeController.php index 6104327..254807b 100644 --- a/app/Node/NodeController.php +++ b/app/Node/NodeController.php @@ -3,12 +3,8 @@ namespace App\Node; use App\Node\NodeRepository; -use App\Node\UseCases\CreateNode; -use App\Node\UseCases\CreateNodeRequest; use App\Text\TextRepository; -use DomainException; use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; class NodeController { @@ -38,41 +34,4 @@ class NodeController $response->getBody()->write(json_encode(array_values($data))); return $response->withHeader('Content-Type', 'application/json'); } - - public function createNode( - Request $request, - Response $response, - CreateNode $createNodeUseCase, - ): Response { - $data = json_decode((string) $request->getBody(), true) ?? []; - $title = $data['title'] ?? ''; - - if (empty($title)) { - $response->getBody()->write(json_encode(['error' => 'Title is required'])); - return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); - } - - $textId = (int) ($data['textId'] ?? 0); - $parentNodeId = isset($data['parentNodeId']) && $data['parentNodeId'] !== null - ? (int) $data['parentNodeId'] - : null; - - try { - $node = $createNodeUseCase->execute(new CreateNodeRequest( - textId: $textId, - title: $title, - parentNodeId: $parentNodeId, - )); - } catch (DomainException $e) { - $response->getBody()->write(json_encode(['error' => $e->getMessage()])); - return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); - } - - $response->getBody()->write(json_encode([ - 'id' => $node->getId(), - 'title' => $node->getTitle(), - 'parentNodeId' => $node->getParentNode()?->getId(), - ])); - return $response->withStatus(201)->withHeader('Content-Type', 'application/json'); - } } diff --git a/bootstrap/app.php b/bootstrap/app.php index cd895b3..37d86df 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -22,6 +22,5 @@ $app->get('/api/texts/{textId}', [TextController::class, 'getText']); $app->post('/api/texts', [TextController::class, 'createText']); $app->get('/api/nodes/{textId}', [NodeController::class, 'getNodesOfText']); -$app->post('/api/nodes', [NodeController::class, 'createNode']); return $app; diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 2c8054c..ba8c069 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -26,6 +26,23 @@ describe('The admin page', () => { cy.contains('Test Text') }) + it('shows one root node and child node on the seeded text page', () => { + cy.visit('/admin/texts/0') + cy.intercept('GET', '/api/nodes/0').as('getNodes') + cy.wait('@getNodes') + cy.get('#text-detail > ul > li > ul > li').should('have.length', 1) + }) + + it('shows one root node on the text page', () => { + cy.visit('/admin/texts') + cy.get('#newTextName').type('My Node Text') + cy.get('#submit').click() + cy.intercept('GET', '/api/nodes/1').as('getNodes') + cy.get('a').contains('My Node Text').click() + cy.wait('@getNodes') + cy.get('#text-detail > ul').should('have.length', 1) + }) + it('navigates to a specific texts page', () => { cy.visit('/admin/texts') cy.get('#newTextName').type('My New Text') diff --git a/cypress/e2e/adminText.cy.js b/cypress/e2e/adminText.cy.js deleted file mode 100644 index 18daef3..0000000 --- a/cypress/e2e/adminText.cy.js +++ /dev/null @@ -1,86 +0,0 @@ -describe('The admin text detail page', () => { - beforeEach(() => { - cy.exec('npm run db:seed') - cy.intercept('GET', '/api/texts/0').as('getText') - cy.intercept('GET', '/api/nodes/0').as('getNodes') - cy.visit('/admin/texts/0') - cy.wait('@getText') - cy.wait('@getNodes') - }) - - afterEach(() => { - cy.exec('npm run db:wipe') - }) - - it('shows the text name as a heading', () => { - cy.get('h1').should('contain', 'test text') - }) - - it('shows the root node', () => { - cy.get('#text-detail li').first().should('contain', 'Chapter 1') - }) - - it('shows a child node under the root node', () => { - cy.get('#text-detail > ul > li > ul > li').should('contain', 'Section 1.1') - }) - - it('shows an "Add child" button on each node', () => { - cy.get('#text-detail li').each(($li) => { - cy.wrap($li).find('button.add-child').should('exist') - }) - }) - - it('clicking "Add child" reveals an inline form', () => { - cy.get('#text-detail li').first().children('button.add-child').click() - cy.get('#text-detail li').first().children('input.child-title').should('be.visible') - cy.get('#text-detail li').first().children('button.save-child').should('be.visible') - }) - - it('can add a child to the root node', () => { - cy.intercept('POST', '/api/nodes').as('createNode') - cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - - cy.get('#text-detail > ul > li').first().children('button.add-child').click() - cy.get('#text-detail > ul > li').first().children('input.child-title').type('New Child Node') - cy.get('#text-detail > ul > li').first().children('button.save-child').click() - - cy.wait('@createNode').its('response.statusCode').should('eq', 201) - cy.wait('@getNodesRefresh') - - cy.get('#text-detail li').should('contain', 'New Child Node') - }) - - it('can add a child to a child node', () => { - cy.intercept('POST', '/api/nodes').as('createNode') - cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - - cy.get('#text-detail > ul > li > ul > li').first().children('button.add-child').click() - cy.get('#text-detail > ul > li > ul > li').first().children('input.child-title').type('Nested Child Node') - cy.get('#text-detail > ul > li > ul > li').first().children('button.save-child').click() - - cy.wait('@createNode').its('response.statusCode').should('eq', 201) - cy.wait('@getNodesRefresh') - - cy.get('#text-detail li').should('contain', 'Nested Child Node') - }) - - it('newly added child persists after page reload', () => { - cy.intercept('POST', '/api/nodes').as('createNode') - cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - - cy.get('#text-detail > ul > li').first().children('button.add-child').click() - cy.get('#text-detail > ul > li').first().children('input.child-title').type('Persistent Child') - cy.get('#text-detail > ul > li').first().children('button.save-child').click() - - cy.wait('@createNode') - cy.wait('@getNodesRefresh') - - cy.intercept('GET', '/api/texts/0').as('getTextReload') - cy.intercept('GET', '/api/nodes/0').as('getNodesReload') - cy.reload() - cy.wait('@getTextReload') - cy.wait('@getNodesReload') - - cy.get('#text-detail li').should('contain', 'Persistent Child') - }) -}) diff --git a/public/js/text.js b/public/js/text.js index fc62744..eafc1c6 100644 --- a/public/js/text.js +++ b/public/js/text.js @@ -8,22 +8,15 @@ document.addEventListener('DOMContentLoaded', () => { h1.textContent = text.name; document.getElementById('text-detail').appendChild(h1); - return fetchAndRenderNodes(textId); - }); -}); - -function fetchAndRenderNodes(textId) { - return fetch('/api/nodes/' + textId) + return fetch('/api/nodes/' + textId ); + }) .then(res => res.json()) .then(nodes => { - const existing = document.querySelector('#text-detail > ul'); - if (existing) existing.remove(); - const tree = buildTree(nodes); - const ul = renderTree(tree, textId); + const ul = renderTree(tree); document.getElementById('text-detail').appendChild(ul); }); -} +}); function buildTree(nodes) { const map = {}; @@ -43,62 +36,15 @@ function buildTree(nodes) { return roots; } -function renderTree(nodes, textId) { +function renderTree(nodes) { const ul = document.createElement('ul'); nodes.forEach(node => { const li = document.createElement('li'); - - const titleSpan = document.createElement('span'); - titleSpan.textContent = node.title; - li.appendChild(titleSpan); - - const addBtn = document.createElement('button'); - addBtn.textContent = 'Add child'; - addBtn.className = 'add-child'; - addBtn.addEventListener('click', () => toggleAddForm(li, node.id, textId)); - li.appendChild(addBtn); - + li.textContent = node.title; if (node.children.length > 0) { - li.appendChild(renderTree(node.children, textId)); + li.appendChild(renderTree(node.children)); } - ul.appendChild(li); }); return ul; } - -function toggleAddForm(li, parentNodeId, textId) { - const existing = li.querySelector('input.child-title'); - if (existing) { - existing.remove(); - li.querySelector('button.save-child').remove(); - return; - } - - const input = document.createElement('input'); - input.type = 'text'; - input.className = 'child-title'; - input.placeholder = 'Node title'; - - const saveBtn = document.createElement('button'); - saveBtn.textContent = 'Save'; - saveBtn.className = 'save-child'; - saveBtn.addEventListener('click', () => { - const title = input.value.trim(); - if (!title) return; - - fetch('/api/nodes', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ textId: parseInt(textId), title, parentNodeId }), - }) - .then(res => { - if (!res.ok) throw new Error('Failed to create node'); - return res.json(); - }) - .then(() => fetchAndRenderNodes(textId)); - }); - - li.appendChild(input); - li.appendChild(saveBtn); -} diff --git a/tests/e2e/Controllers/NodeControllerTest.php b/tests/e2e/Controllers/NodeControllerTest.php deleted file mode 100644 index ab01495..0000000 --- a/tests/e2e/Controllers/NodeControllerTest.php +++ /dev/null @@ -1,159 +0,0 @@ -textRepo = new FakeTextRepository; - $this->textRepo->create(new CreateTextDto(name: 'test text')); - - $this->nodeRepo = new FakeNodeRepository; - - $this->controller = new NodeController($this->nodeRepo, $this->textRepo); - } - - public function test_get_nodes_of_text_returns_flat_array(): void - { - $text = $this->textRepo->find(0); - $this->nodeRepo->create(new CreateNodeDto( - text: $text, - title: 'Root Node', - parentNode: null, - )); - - $response = $this->controller->getNodesOfText(new Response(), 0); - - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals( - json_encode([ - ['id' => 0, 'title' => 'Root Node', 'parentNodeId' => null], - ]), - $response->getBody(), - ); - } - - public function test_get_nodes_of_text_returns_empty_array_when_no_nodes(): void - { - $response = $this->controller->getNodesOfText(new Response(), 0); - - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(json_encode([]), $response->getBody()); - } - - public function test_get_nodes_of_text_returns_404_for_unknown_text(): void - { - $response = $this->controller->getNodesOfText(new Response(), 99); - - $this->assertEquals(404, $response->getStatusCode()); - } - - public function test_get_nodes_includes_parent_node_id(): void - { - $text = $this->textRepo->find(0); - $rootNode = $this->nodeRepo->create(new CreateNodeDto( - text: $text, - title: 'Root Node', - parentNode: null, - )); - $this->nodeRepo->create(new CreateNodeDto( - text: $text, - title: 'Child Node', - parentNode: $rootNode, - )); - - $response = $this->controller->getNodesOfText(new Response(), 0); - $body = json_decode($response->getBody(), true); - - $this->assertEquals(0, $body[1]['parentNodeId']); - } - - public function test_create_node_returns_created_node(): void - { - $text = $this->textRepo->find(0); - $rootNode = $this->nodeRepo->create(new CreateNodeDto( - text: $text, - title: 'Root Node', - parentNode: null, - )); - - $body = (new StreamFactory())->createStream(json_encode([ - 'textId' => 0, - 'title' => 'Child Node', - 'parentNodeId' => $rootNode->getId(), - ])); - $request = (new ServerRequestFactory()) - ->createServerRequest('POST', 'http://localhost/api/nodes') - ->withHeader('Content-Type', 'application/json') - ->withBody($body); - - $response = $this->controller->createNode( - $request, - new Response(), - new CreateNode($this->nodeRepo, $this->textRepo), - ); - - $this->assertEquals(201, $response->getStatusCode()); - $body = json_decode($response->getBody(), true); - $this->assertEquals('Child Node', $body['title']); - $this->assertEquals($rootNode->getId(), $body['parentNodeId']); - $this->assertArrayHasKey('id', $body); - } - - public function test_create_node_returns_400_when_title_missing(): void - { - $body = (new StreamFactory())->createStream(json_encode([ - 'textId' => 0, - 'parentNodeId' => null, - ])); - $request = (new ServerRequestFactory()) - ->createServerRequest('POST', 'http://localhost/api/nodes') - ->withHeader('Content-Type', 'application/json') - ->withBody($body); - - $response = $this->controller->createNode( - $request, - new Response(), - new CreateNode($this->nodeRepo, $this->textRepo), - ); - - $this->assertEquals(400, $response->getStatusCode()); - } - - public function test_create_node_returns_404_when_text_not_found(): void - { - $body = (new StreamFactory())->createStream(json_encode([ - 'textId' => 99, - 'title' => 'Some Node', - 'parentNodeId' => null, - ])); - $request = (new ServerRequestFactory()) - ->createServerRequest('POST', 'http://localhost/api/nodes') - ->withHeader('Content-Type', 'application/json') - ->withBody($body); - - $response = $this->controller->createNode( - $request, - new Response(), - new CreateNode($this->nodeRepo, $this->textRepo), - ); - - $this->assertEquals(404, $response->getStatusCode()); - } -}