test set layout browsing
This commit is contained in:
parent
936855efab
commit
eb7ac7d261
8 changed files with 619 additions and 3 deletions
|
|
@ -3,6 +3,8 @@
|
||||||
namespace Tests\Feature\Database;
|
namespace Tests\Feature\Database;
|
||||||
|
|
||||||
use App\Auth\PasswordHasher;
|
use App\Auth\PasswordHasher;
|
||||||
|
use App\Element\Element;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
use App\Set\SetRepository;
|
use App\Set\SetRepository;
|
||||||
use App\Shared\ValueObject\EmailAddress;
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
use App\User\UserRepository;
|
use App\User\UserRepository;
|
||||||
|
|
@ -60,4 +62,60 @@ class DatabaseSeederTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_seeds_elements_for_every_set_idempotently(): void
|
||||||
|
{
|
||||||
|
$this->seed();
|
||||||
|
$this->seed();
|
||||||
|
|
||||||
|
$sets = app(SetRepository::class)->all();
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$expectedElements = [
|
||||||
|
'Bible' => [
|
||||||
|
'Chapter 1:chapter:Creation',
|
||||||
|
'Chapter 2:chapter:Creation',
|
||||||
|
'Creation:portion:Genesis',
|
||||||
|
'Exodus:book:root',
|
||||||
|
'Genesis:book:root',
|
||||||
|
'Noah:portion:Genesis',
|
||||||
|
'Shemot:portion:Exodus',
|
||||||
|
],
|
||||||
|
'Course' => [
|
||||||
|
'Applied Practice:module:root',
|
||||||
|
'Core Concepts:lesson:Foundations',
|
||||||
|
'Final Review:lesson:Applied Practice',
|
||||||
|
'Foundations:module:root',
|
||||||
|
'Guided Exercise:lesson:Applied Practice',
|
||||||
|
'Welcome:lesson:Foundations',
|
||||||
|
],
|
||||||
|
'Fitness Program' => [
|
||||||
|
'Build Phase:phase:root',
|
||||||
|
'Foundation Phase:phase:root',
|
||||||
|
'Full Body Circuit:workout:Build Phase',
|
||||||
|
'Hip Flow:exercise:Mobility Day',
|
||||||
|
'Mobility Day:workout:Foundation Phase',
|
||||||
|
'Push-up:exercise:Strength Day',
|
||||||
|
'Squat:exercise:Strength Day',
|
||||||
|
'Strength Day:workout:Foundation Phase',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('elements', 21);
|
||||||
|
|
||||||
|
foreach ($sets as $set) {
|
||||||
|
$signatures = array_map(function (Element $element): string {
|
||||||
|
$parentName = $element->getParentElement()?->getName()
|
||||||
|
?? 'root';
|
||||||
|
|
||||||
|
return "{$element->getName()}:{$element->getKind()}"
|
||||||
|
. ":{$parentName}";
|
||||||
|
}, $elementRepository->findBySet($set));
|
||||||
|
sort($signatures);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
$expectedElements[$set->getName()],
|
||||||
|
$signatures,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,45 @@ class EloquentElementRepositoryTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testItListsEveryElementForASet(): void
|
||||||
|
{
|
||||||
|
$bible = $this->createSet('Bible');
|
||||||
|
$course = $this->createSet('Course');
|
||||||
|
$repository = app(ElementRepository::class);
|
||||||
|
$genesis = $repository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$repository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$repository->create(new CreateElementDto(
|
||||||
|
set: $course,
|
||||||
|
name: 'Foundations',
|
||||||
|
kind: 'module',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$elements = $repository->findBySet($bible);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['Genesis', 'Creation'],
|
||||||
|
array_map(function ($element): string {
|
||||||
|
return $element->getName();
|
||||||
|
}, $elements),
|
||||||
|
);
|
||||||
|
$this->assertNull($elements[0]->getParentElement());
|
||||||
|
$this->assertSame(
|
||||||
|
$genesis->getId(),
|
||||||
|
$elements[1]->getParentElement()?->getId(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function createSet(string $name): Set
|
private function createSet(string $name): Set
|
||||||
{
|
{
|
||||||
$creator = app(UserRepository::class)->create(new CreateUserDto(
|
$creator = app(UserRepository::class)->create(new CreateUserDto(
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,23 @@ class EloquentSetRepositoryTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_finds_a_set_by_id(): void
|
||||||
|
{
|
||||||
|
$creator = $this->createUser('creator@example.com');
|
||||||
|
$repository = app(SetRepository::class);
|
||||||
|
$createdSet = $repository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $creator,
|
||||||
|
));
|
||||||
|
|
||||||
|
$foundSet = $repository->find($createdSet->getId());
|
||||||
|
|
||||||
|
$this->assertNotNull($foundSet);
|
||||||
|
$this->assertSame($createdSet->getId(), $foundSet->getId());
|
||||||
|
$this->assertSame('Bible', $foundSet->getName());
|
||||||
|
$this->assertNull($repository->find(999));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_rejects_duplicate_set_names(): void
|
public function test_it_rejects_duplicate_set_names(): void
|
||||||
{
|
{
|
||||||
$creator = $this->createUser('creator@example.com');
|
$creator = $this->createUser('creator@example.com');
|
||||||
|
|
|
||||||
171
backend/tests/Feature/Set/GetSetLayoutEndpointTest.php
Normal file
171
backend/tests/Feature/Set/GetSetLayoutEndpointTest.php
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Set;
|
||||||
|
|
||||||
|
use App\Auth\CreateSessionDto;
|
||||||
|
use App\Auth\SessionRepository;
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
|
use App\Http\Middleware\AuthMiddleware;
|
||||||
|
use App\Set\CreateSetDto;
|
||||||
|
use App\Set\Set;
|
||||||
|
use App\Set\SetRepository;
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\CreateUserDto;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class GetSetLayoutEndpointTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_it_returns_a_sets_ordered_element_layout(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
$set = $this->createSet($user, 'Bible');
|
||||||
|
$repository = app(ElementRepository::class);
|
||||||
|
$genesis = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Exodus',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$creation = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$chapter = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Chapter 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
));
|
||||||
|
$this->createSession($user);
|
||||||
|
|
||||||
|
$response = $this->credentialedGet("/api/sets/{$set->getId()}");
|
||||||
|
|
||||||
|
$response->assertOk()->assertExactJson([
|
||||||
|
'set' => [
|
||||||
|
'id' => $set->getId(),
|
||||||
|
'name' => 'Bible',
|
||||||
|
],
|
||||||
|
'elements' => [
|
||||||
|
[
|
||||||
|
'id' => $genesis->getId(),
|
||||||
|
'name' => 'Genesis',
|
||||||
|
'kind' => 'book',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'id' => $creation->getId(),
|
||||||
|
'name' => 'Creation',
|
||||||
|
'kind' => 'portion',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'id' => $chapter->getId(),
|
||||||
|
'name' => 'Chapter 1',
|
||||||
|
'kind' => 'chapter',
|
||||||
|
'children' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $genesis->getId() + 1,
|
||||||
|
'name' => 'Exodus',
|
||||||
|
'kind' => 'book',
|
||||||
|
'children' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_an_empty_element_layout(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
$set = $this->createSet($user, 'Empty set');
|
||||||
|
$this->createSession($user);
|
||||||
|
|
||||||
|
$response = $this->credentialedGet("/api/sets/{$set->getId()}");
|
||||||
|
|
||||||
|
$response->assertOk()->assertExactJson([
|
||||||
|
'set' => [
|
||||||
|
'id' => $set->getId(),
|
||||||
|
'name' => 'Empty set',
|
||||||
|
],
|
||||||
|
'elements' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_not_found_for_an_unknown_set(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
$this->createSession($user);
|
||||||
|
|
||||||
|
$response = $this->credentialedGet('/api/sets/999');
|
||||||
|
|
||||||
|
$response->assertNotFound()->assertExactJson([
|
||||||
|
'error' => 'set not found',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_an_unauthenticated_request(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/sets/1');
|
||||||
|
|
||||||
|
$response->assertStatus(401)->assertExactJson([
|
||||||
|
'error' => 'unauthenticated',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createUser(): User
|
||||||
|
{
|
||||||
|
return app(UserRepository::class)->create(new CreateUserDto(
|
||||||
|
email: new EmailAddress('reader@example.com'),
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSet(User $user, string $name): Set
|
||||||
|
{
|
||||||
|
return app(SetRepository::class)->create(new CreateSetDto(
|
||||||
|
name: $name,
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSession(User $user): void
|
||||||
|
{
|
||||||
|
$createdAt = new DateTimeImmutable(
|
||||||
|
'2026-08-03T12:00:00',
|
||||||
|
new DateTimeZone('UTC'),
|
||||||
|
);
|
||||||
|
app(SessionRepository::class)->create(new CreateSessionDto(
|
||||||
|
token: 'valid-token',
|
||||||
|
user: $user,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
expiresAt: $createdAt->modify('+7 days'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function credentialedGet(string $uri): \Illuminate\Testing\TestResponse
|
||||||
|
{
|
||||||
|
return $this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'valid-token',
|
||||||
|
)->getJson($uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php
Normal file
136
backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Set\UseCases;
|
||||||
|
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Set\CreateSetDto;
|
||||||
|
use App\Set\UseCases\GetSetLayout\GetSetLayout;
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\User;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Tests\Fakes\FakeElementRepository;
|
||||||
|
use Tests\Fakes\FakeSetRepository;
|
||||||
|
|
||||||
|
class GetSetLayoutTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_builds_an_ordered_element_tree(): void
|
||||||
|
{
|
||||||
|
$creator = new User(
|
||||||
|
id: 7,
|
||||||
|
email: new EmailAddress('creator@example.com'),
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
);
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$elementRepository = new FakeElementRepository;
|
||||||
|
$bible = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $creator,
|
||||||
|
));
|
||||||
|
$course = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Course',
|
||||||
|
creator: $creator,
|
||||||
|
));
|
||||||
|
$genesis = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Exodus',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$creation = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Noah',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $bible,
|
||||||
|
name: 'Chapter 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
));
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $course,
|
||||||
|
name: 'Foundations',
|
||||||
|
kind: 'module',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$layout = (new GetSetLayout(
|
||||||
|
$setRepository,
|
||||||
|
$elementRepository,
|
||||||
|
))->execute($bible->getId());
|
||||||
|
|
||||||
|
$this->assertSame('Bible', $layout->getSet()->getName());
|
||||||
|
$this->assertSame(
|
||||||
|
['Genesis', 'Exodus'],
|
||||||
|
$this->nodeNames($layout->getElements()),
|
||||||
|
);
|
||||||
|
$genesisNode = $layout->getElements()[0];
|
||||||
|
$this->assertSame(
|
||||||
|
['Creation', 'Noah'],
|
||||||
|
$this->nodeNames($genesisNode->getChildren()),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
['Chapter 1'],
|
||||||
|
$this->nodeNames(
|
||||||
|
$genesisNode->getChildren()[0]->getChildren(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_an_empty_layout(): void
|
||||||
|
{
|
||||||
|
$creator = new User(
|
||||||
|
id: 7,
|
||||||
|
email: new EmailAddress('creator@example.com'),
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
);
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Empty set',
|
||||||
|
creator: $creator,
|
||||||
|
));
|
||||||
|
|
||||||
|
$layout = (new GetSetLayout(
|
||||||
|
$setRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
))->execute($set->getId());
|
||||||
|
|
||||||
|
$this->assertSame([], $layout->getElements());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_an_unknown_set(): void
|
||||||
|
{
|
||||||
|
$this->expectException(NotFoundException::class);
|
||||||
|
$this->expectExceptionMessage('set not found');
|
||||||
|
|
||||||
|
(new GetSetLayout(
|
||||||
|
new FakeSetRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
))->execute(999);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<\App\Set\UseCases\GetSetLayout\ElementLayoutNode> $nodes
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function nodeNames(array $nodes): array
|
||||||
|
{
|
||||||
|
return array_map(function ($node): string {
|
||||||
|
return $node->getElement()->getName();
|
||||||
|
}, $nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,13 @@ describe('guest authentication pages', () => {
|
||||||
cy.location('search').should('include', 'redirect=/dashboard')
|
cy.location('search').should('include', 'redirect=/dashboard')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('redirects guests away from a protected set layout', () => {
|
||||||
|
cy.visit('/sets/41')
|
||||||
|
|
||||||
|
cy.location('pathname').should('equal', '/login')
|
||||||
|
cy.location('search').should('include', 'redirect=/sets/41')
|
||||||
|
})
|
||||||
|
|
||||||
it('shows the login form and links to signup', () => {
|
it('shows the login form and links to signup', () => {
|
||||||
cy.visit('/login')
|
cy.visit('/login')
|
||||||
|
|
||||||
|
|
|
||||||
183
frontend/website/cypress/e2e/set-layout.cy.ts
Normal file
183
frontend/website/cypress/e2e/set-layout.cy.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
const authenticatedUser = {
|
||||||
|
id: 7,
|
||||||
|
email: 'user@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const bibleLayout = {
|
||||||
|
set: { id: 41, name: 'Bible' },
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Chapter 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ id: 5, name: 'Noah', kind: 'portion', children: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ id: 2, name: 'Exodus', kind: 'book', children: [] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function interceptAuthenticatedUser(): void {
|
||||||
|
cy.intercept('GET', '**/api/me', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: { user: authenticatedUser },
|
||||||
|
}).as('me')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('set element layout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptAuthenticatedUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens a set from the dashboard and shows its full hierarchy', () => {
|
||||||
|
cy.intercept('GET', '**/api/sets', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: { sets: [{ id: 41, name: 'Bible' }] },
|
||||||
|
}).as('sets')
|
||||||
|
cy.intercept('GET', '**/api/sets/41', (request) => {
|
||||||
|
expect(request.headers.accept).to.equal('application/json')
|
||||||
|
request.reply({ statusCode: 200, body: bibleLayout })
|
||||||
|
}).as('layout')
|
||||||
|
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.wait('@me')
|
||||||
|
cy.wait('@sets')
|
||||||
|
cy.contains('a', 'Bible').click()
|
||||||
|
cy.wait('@layout')
|
||||||
|
|
||||||
|
cy.location('pathname').should('equal', '/sets/41')
|
||||||
|
cy.get('h1').should('have.text', 'Bible')
|
||||||
|
cy.contains('a', 'Back to sets').should('have.attr', 'href', '/dashboard')
|
||||||
|
cy.get('ol[aria-label="Bible element layout"] > li').then(($nodes) => {
|
||||||
|
expect([...$nodes].map((node) => node.dataset.elementId)).to.deep.equal([
|
||||||
|
'1',
|
||||||
|
'2',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
cy.get('[data-element-id="1"] > .element-node__card')
|
||||||
|
.should('contain.text', 'Genesis')
|
||||||
|
.and('contain.text', 'book')
|
||||||
|
cy.get('[data-element-id="1"] > ol > li').then(($nodes) => {
|
||||||
|
expect([...$nodes].map((node) => node.dataset.elementId)).to.deep.equal([
|
||||||
|
'3',
|
||||||
|
'5',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
cy.get('[data-element-id="3"] > ol > li')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.and('have.attr', 'data-element-id', '4')
|
||||||
|
cy.contains('.element-node__card', 'Chapter 1')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('contain.text', 'chapter')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows loading and empty layout states', () => {
|
||||||
|
cy.intercept('GET', '**/api/sets/41', {
|
||||||
|
delay: 500,
|
||||||
|
statusCode: 200,
|
||||||
|
body: { set: { id: 41, name: 'Empty set' }, elements: [] },
|
||||||
|
}).as('layout')
|
||||||
|
|
||||||
|
cy.visit('/sets/41')
|
||||||
|
cy.wait('@me')
|
||||||
|
cy.get('[role="status"]').should('have.text', 'Loading set layout...')
|
||||||
|
cy.wait('@layout')
|
||||||
|
|
||||||
|
cy.get('h1').should('have.text', 'Empty set')
|
||||||
|
cy.get('[role="status"]').should(
|
||||||
|
'have.text',
|
||||||
|
'This set does not have any elements yet.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retries after a layout request fails', () => {
|
||||||
|
let requestCount = 0
|
||||||
|
cy.intercept('GET', '**/api/sets/41', (request) => {
|
||||||
|
requestCount += 1
|
||||||
|
request.alias = `layout${requestCount}`
|
||||||
|
|
||||||
|
if (requestCount === 1) {
|
||||||
|
request.reply({ statusCode: 500 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.reply({ statusCode: 200, body: bibleLayout })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit('/sets/41')
|
||||||
|
cy.wait('@me')
|
||||||
|
cy.wait('@layout1')
|
||||||
|
|
||||||
|
cy.get('[role="alert"]').should(
|
||||||
|
'contain.text',
|
||||||
|
"We couldn't load this set's layout.",
|
||||||
|
)
|
||||||
|
cy.contains('button', 'Try again').click()
|
||||||
|
cy.wait('@layout2')
|
||||||
|
|
||||||
|
cy.get('h1').should('have.text', 'Bible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('distinguishes missing sets from malformed responses', () => {
|
||||||
|
cy.intercept('GET', '**/api/sets/41', {
|
||||||
|
statusCode: 404,
|
||||||
|
body: { error: 'set not found' },
|
||||||
|
}).as('missing')
|
||||||
|
|
||||||
|
cy.visit('/sets/41')
|
||||||
|
cy.wait('@me')
|
||||||
|
cy.wait('@missing')
|
||||||
|
|
||||||
|
cy.get('h1').should('have.text', 'Set not found')
|
||||||
|
cy.get('[role="alert"]').should(
|
||||||
|
'contain.text',
|
||||||
|
"The set you're looking for is not available.",
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept('GET', '**/api/sets/42', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: { set: { id: 42, name: 12 }, elements: [] },
|
||||||
|
}).as('malformed')
|
||||||
|
cy.visit('/sets/42')
|
||||||
|
cy.wait('@malformed')
|
||||||
|
|
||||||
|
cy.get('[role="alert"]').should(
|
||||||
|
'contain.text',
|
||||||
|
"We couldn't load this set's layout.",
|
||||||
|
)
|
||||||
|
cy.contains('button', 'Try again').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps a deeply nested layout within a mobile viewport', () => {
|
||||||
|
cy.viewport(390, 844)
|
||||||
|
cy.intercept('GET', '**/api/sets/41', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: bibleLayout,
|
||||||
|
}).as('layout')
|
||||||
|
|
||||||
|
cy.visit('/sets/41')
|
||||||
|
cy.wait('@me')
|
||||||
|
cy.wait('@layout')
|
||||||
|
|
||||||
|
cy.contains('.element-node__card', 'Chapter 1').should('be.visible')
|
||||||
|
cy.document().then((document) => {
|
||||||
|
expect(document.documentElement.scrollWidth).to.be.at.most(
|
||||||
|
document.documentElement.clientWidth,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -15,7 +15,7 @@ describe('sets dashboard', () => {
|
||||||
interceptAuthenticatedUser()
|
interceptAuthenticatedUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows every available set by name', () => {
|
it('shows every available set as a detail link', () => {
|
||||||
cy.intercept('GET', '**/api/sets', (request) => {
|
cy.intercept('GET', '**/api/sets', (request) => {
|
||||||
expect(request.headers.accept).to.equal('application/json')
|
expect(request.headers.accept).to.equal('application/json')
|
||||||
request.reply({
|
request.reply({
|
||||||
|
|
@ -46,8 +46,13 @@ describe('sets dashboard', () => {
|
||||||
.should('not.contain.text', '41')
|
.should('not.contain.text', '41')
|
||||||
.and('not.contain.text', '58')
|
.and('not.contain.text', '58')
|
||||||
.and('not.contain.text', '92')
|
.and('not.contain.text', '92')
|
||||||
.find('a, button')
|
cy.contains('a', 'Bible').should('have.attr', 'href', '/sets/41')
|
||||||
.should('not.exist')
|
cy.contains('a', 'Course').should('have.attr', 'href', '/sets/58')
|
||||||
|
cy.contains('a', 'Fitness Program').should(
|
||||||
|
'have.attr',
|
||||||
|
'href',
|
||||||
|
'/sets/92',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows loading and empty catalog states', () => {
|
it('shows loading and empty catalog states', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue