const authenticatedUser = { id: 7, email: 'user@example.com', } const bibleLayout = { set: { id: 41, name: 'Bible' }, levels: [ { id: 1, kind: 'book', depth: 0, elementCount: 2 }, { id: 2, kind: 'portion', depth: 1, elementCount: 2 }, { id: 3, kind: 'Chapter_sections-v2', depth: 2, elementCount: 2 }, ], elements: [ { id: 1, name: 'Genesis', kind: 'book', levelId: 1, children: [ { id: 3, name: 'Creation', kind: 'portion', levelId: 2, children: [ { id: 4, name: 'Chapter 1', kind: 'Chapter_sections-v2', levelId: 3, children: [], }, ], }, ], }, { id: 2, name: 'Exodus', kind: 'book', levelId: 1, children: [ { id: 5, name: 'Shemot', kind: 'portion', levelId: 2, children: [ { id: 6, name: 'Chapter 1', kind: 'Chapter_sections-v2', levelId: 3, children: [], }, ], }, ], }, ], } const scheduleDetail = { schedule: { id: 73, set: { name: 'Bible' }, elementKind: 'Chapter_sections-v2', startDate: '2026-08-10', targetDate: '2026-08-12', assignmentCount: 2, days: [ { date: '2026-08-10', assignments: [ { id: 1, element: { name: 'Chapter 1', kind: 'Chapter_sections-v2', path: ['Genesis', 'Creation', 'Chapter 1'], }, }, ], }, { date: '2026-08-11', assignments: [] }, { date: '2026-08-12', assignments: [ { id: 2, element: { name: 'Chapter 1', kind: 'Chapter_sections-v2', path: ['Exodus', 'Shemot', 'Chapter 1'], }, }, ], }, ], }, } function interceptAuthenticatedUser(): void { cy.intercept('GET', '**/api/me', { statusCode: 200, body: { user: authenticatedUser }, }).as('me') } describe('set scheduling', () => { beforeEach(() => { interceptAuthenticatedUser() }) it('creates a schedule from a set and shows every day', () => { cy.intercept('GET', '**/api/sets/41', { statusCode: 200, body: bibleLayout, }).as('layout') cy.intercept('POST', '**/api/schedules', (request) => { expect(request.headers.accept).to.equal('application/json') expect(request.body).to.deep.equal({ setId: 41, levelId: 3, startDate: '2026-08-10', targetDate: '2026-08-12', }) request.reply({ statusCode: 201, body: scheduleDetail }) }).as('createSchedule') cy.visit('/sets/41') cy.wait('@me') cy.wait('@layout') cy.contains('a', 'Schedule this set') .should('have.attr', 'href', '/sets/41/schedules/new') .click() cy.location('pathname').should('equal', '/sets/41/schedules/new') cy.get('h1').should('have.text', 'Schedule Bible') cy.get('#schedule-level option').then(($options) => { expect([...$options].map((option) => option.textContent?.trim())).to.deep.equal( ['Choose a level', 'Book (2)', 'Portion (2)', 'Chapter_sections-v2 (2)'], ) }) cy.get('#schedule-level').select('3') cy.get('#schedule-start-date').type('2026-08-10') cy.get('#schedule-target-date').type('2026-08-12') cy.get('form').submit() cy.wait('@createSchedule') cy.location('pathname').should('equal', '/schedules/73') cy.get('h1').should('have.text', 'Bible schedule') cy.get('[data-schedule-day]').should('have.length', 3) cy.get('[data-schedule-date="2026-08-10"]') .should('contain.text', 'Genesis / Creation / Chapter 1') .find('.assignment-kind') .should('have.text', 'Chapter_sections-v2') cy.get('[data-schedule-date="2026-08-11"]') .should('contain.text', 'Rest day') .and('not.contain.text', 'Chapter 1') cy.get('[data-schedule-date="2026-08-12"]').should( 'contain.text', 'Exodus / Shemot / Chapter 1', ) }) it('validates the schedule form before submitting', () => { cy.intercept('GET', '**/api/sets/41', { statusCode: 200, body: bibleLayout, }).as('layout') cy.intercept('POST', '**/api/schedules').as('createSchedule') cy.visit('/sets/41/schedules/new') cy.wait('@me') cy.wait('@layout') cy.get('form').submit() cy.get('#schedule-level-error') .should('have.text', 'Choose a level to schedule.') .and('be.visible') cy.get('#schedule-start-date-error') .should('have.text', 'Choose a start date.') .and('be.visible') cy.get('#schedule-target-date-error') .should('have.text', 'Choose a target date.') .and('be.visible') cy.get('@createSchedule.all').should('have.length', 0) cy.get('#schedule-level').select('3') cy.get('#schedule-start-date').type('2026-08-12') cy.get('#schedule-target-date').type('2026-08-10') cy.get('form').submit() cy.get('#schedule-target-date-error').should( 'have.text', 'Target date cannot be before the start date.', ) cy.get('@createSchedule.all').should('have.length', 0) }) it('loads a persisted schedule directly and handles missing schedules', () => { cy.intercept('GET', '**/api/schedules/73', { statusCode: 200, body: scheduleDetail, }).as('schedule') cy.visit('/schedules/73') cy.wait('@me') cy.wait('@schedule') cy.get('h1').should('have.text', 'Bible schedule') cy.get('.eyebrow').should('have.text', 'Chapter_sections-v2 plan') cy.contains('2 Chapter_sections-v2 assignments across 3 days').should('be.visible') cy.intercept('GET', '**/api/schedules/74', { statusCode: 404, body: { error: 'schedule not found' }, }).as('missingSchedule') cy.visit('/schedules/74') cy.wait('@missingSchedule') cy.get('h1').should('have.text', 'Schedule not found') }) it('lists the users schedules on the dashboard', () => { cy.intercept('GET', '**/api/sets', { statusCode: 200, body: { sets: [] }, }).as('sets') cy.intercept('GET', '**/api/schedules', (request) => { expect(request.headers.accept).to.equal('application/json') request.reply({ statusCode: 200, body: { schedules: [ { id: 73, set: { name: 'Bible' }, elementKind: 'Chapter_sections-v2', startDate: '2026-08-10', targetDate: '2026-08-12', assignmentCount: 2, }, ], }, }) }).as('schedules') cy.visit('/dashboard') cy.wait('@me') cy.wait('@sets') cy.wait('@schedules') cy.get('#schedules-heading').should('have.text', 'Your schedules') cy.get('ul[aria-label="Your schedules"]') .should('contain.text', 'Bible') .and('contain.text', '2 assignments') cy.get('.schedule-card__heading p').should('have.text', 'Chapter_sections-v2') cy.contains('a', 'Bible').should('have.attr', 'href', '/schedules/73') }) })