Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 237 additions & 10 deletions client-v3/e2e/tests/10-show-config-script.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
});
});

test('can navigate to page via Go to Page dialog', async () => {

Check failure on line 76 in client-v3/e2e/tests/10-show-config-script.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ73DMxOuH9Pu861zbAz&open=AZ73DMxOuH9Pu861zbAz&pullRequest=1242
await page.click('button:has-text("Go to Page")');
await waitForModal(page, 'Go to Page');
await page.fill('.modal.show input[type="number"]', '1');
Expand All @@ -97,11 +97,31 @@
await lineEditor.locator('select').nth(0).selectOption({ label: 'Act 1' });
await lineEditor.locator('select').nth(1).selectOption({ label: 'Scene 1' });

await page.locator('select').filter({ hasText: 'Hamlet' }).selectOption({ label: 'Hamlet' });
await lineEditor
.locator('select')
.filter({ hasText: 'Hamlet' })
.selectOption({ label: 'Hamlet' });
await page.locator('input[type="text"]:visible').last().fill('To be or not to be');

await page.locator('button:has-text("Done")').first().click();
// ScriptEditor auto-adds a blank dialogue line after Done — delete it before asserting
// ScriptEditor auto-adds a blank dialogue editor after Done — fill it in as a second line so
// the bulk edit tests below can use a two-line range without needing the split dropdown.
await expect(page.locator('button:has-text("Done")').first()).toBeVisible({ timeout: 5_000 });

const lineEditor2 = page
.locator('div.row')
.filter({ has: page.locator('button:has-text("Done")') })
.first();
await lineEditor2.locator('select').nth(0).selectOption({ label: 'Act 1' });
await lineEditor2.locator('select').nth(1).selectOption({ label: 'Scene 1' });
await lineEditor2
.locator('select')
.filter({ hasText: 'Hamlet' })
.selectOption({ label: 'Hamlet' });
await page.locator('input[type="text"]:visible').last().fill('That is the question');

await page.locator('button:has-text("Done")').first().click();
// Delete the third auto-added blank editor
await page.locator('button.btn-danger:has-text("Delete")').first().click();
await expect(page.locator('button:has-text("Done")')).not.toBeVisible({ timeout: 5_000 });

Expand All @@ -118,6 +138,59 @@
await expect(
page.locator('.viewable-line').filter({ hasText: 'To be or not to be' })
).toBeVisible({ timeout: 10_000 });
await expect(
page.locator('.viewable-line').filter({ hasText: 'That is the question' })
).toBeVisible({ timeout: 10_000 });
});

// ── Bulk edit ─────────────────────────────────────────────────────────────

test('bulk edit mode is accessible and shows Start/End buttons', async () => {
// Still in edit mode from the save test, which saved two viewable dialogue lines.
await page.click('button:has-text("Bulk Edit")');
// Two ScriptLineViewer rows → two Start/End button pairs
await expect(page.getByRole('button', { name: 'Start', exact: true }).first()).toBeVisible({
timeout: 5_000,
});
await expect(page.getByRole('button', { name: 'End', exact: true }).first()).toBeVisible({
timeout: 5_000,
});
});

test('bulk edit opens the Bulk Edit modal when start and end span two different lines', async () => {

Check failure on line 160 in client-v3/e2e/tests/10-show-config-script.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ73DMxOuH9Pu861zbA0&open=AZ73DMxOuH9Pu861zbA0&pullRequest=1242
// Start on the first line, End on the last line (different indices → valid range)
await page.getByRole('button', { name: 'Start', exact: true }).first().click();
await page.getByRole('button', { name: 'End', exact: true }).last().click();
await waitForModal(page, 'Bulk Edit');
});

test('bulk edit can assign a character to part 1', async () => {
await page.locator('.modal.show #bulk-part-input').selectOption({ label: 'Part 1' });
// Select Alice — a different character from Hamlet (who is already assigned) so the apply
// produces a real change that scriptChanges can detect via deep equality.
await page.locator('.modal.show #bulk-char-input').selectOption({ label: 'Alice' });
await confirmModal(page);
await waitForModalClosed(page);
// After apply, bulk edit mode exits automatically
await expect(page.getByRole('button', { name: 'Bulk Edit', exact: true })).toBeVisible({
timeout: 5_000,
});
});

test('bulk edit stops when Exit Bulk Edit is clicked', async () => {
await page.click('button:has-text("Bulk Edit")');
await expect(page.getByRole('button', { name: 'Start', exact: true }).first()).toBeVisible({
timeout: 5_000,
});
await page.click('button:has-text("Exit Bulk Edit")');
await expect(page.getByRole('button', { name: 'Start', exact: true })).not.toBeVisible({
timeout: 3_000,
});
await page.click('button:has-text("Stop Editing")');
await confirmDialog(page);
await expect(page.getByRole('button', { name: 'Edit', exact: true })).toBeVisible({
timeout: 10_000,
});
});

// ── Cut mode ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -228,11 +301,12 @@

test('adds a cue to the script line', async () => {
await page.locator('.add-cue-btn').first().click();
await waitForModal(page, 'Add New Cue');
await waitForModal(page, 'Add Cue');
// Individual Cue tab is active by default — no extra click needed
// Scope to the visible modal's select to avoid matching the hidden "Add Cue Type" modal
// dialog which BVN assigns id="new-cue-type" via its auto-ID scheme.
await page.locator('.modal.show select#new-cue-type').selectOption({ index: 1 });
await page.fill('#new-cue-ident', '001');
await page.locator('.modal.show #new-cue-ident').fill('001');
await confirmModal(page);
await waitForModalClosed(page);
// Wait for the actual cue button (not the add-cue-btn which shares the cue-button class)
Expand All @@ -246,7 +320,7 @@
// Use :not(.add-cue-btn) to target the real cue button, not the add button
await page.locator('.cue-button:not(.add-cue-btn)').first().click();
await waitForModal(page, 'Edit Cue');
await page.fill('#edit-cue-ident', '002');
await page.locator('.modal.show #edit-cue-ident').fill('002');
await confirmModal(page);
await waitForModalClosed(page);
await expect(page.locator('.cue-button:not(.add-cue-btn)').first()).toBeVisible({
Expand All @@ -256,11 +330,14 @@

test('can add a cue using Enter key in Add New Cue modal', async () => {
await page.locator('.add-cue-btn').first().click();
await waitForModal(page, 'Add New Cue');
await waitForModal(page, 'Add Cue');
// Individual Cue tab is active by default — no extra click needed
await page.locator('.modal.show select#new-cue-type').selectOption({ index: 1 });
await page.fill('#new-cue-ident', '003');
await page.locator('.modal.show #new-cue-ident').fill('003');
// Enter key submits the form (fix: BForm @submit bound to onSubmitNew)
await page.locator('#new-cue-ident').press('Enter');
// Scope to .modal.show to avoid strict-mode violation from other Add New Cue modal instances
// in the DOM (one per viewable script line — BVN v-show keeps them all present).
await page.locator('.modal.show #new-cue-ident').press('Enter');
await waitForModalClosed(page);
await expect(page.locator('.cue-button:not(.add-cue-btn)')).toHaveCount(2, { timeout: 5_000 });
});
Expand All @@ -269,9 +346,9 @@
// Click the second cue button (003)
await page.locator('.cue-button:not(.add-cue-btn)').last().click();
await waitForModal(page, 'Edit Cue');
await page.fill('#edit-cue-ident', '004');
await page.locator('.modal.show #edit-cue-ident').fill('004');
// Enter key submits the form (fix: BForm @submit bound to onSubmitEdit)
await page.locator('#edit-cue-ident').press('Enter');
await page.locator('.modal.show #edit-cue-ident').press('Enter');
await waitForModalClosed(page);
await expect(page.locator('.cue-button:not(.add-cue-btn)')).toHaveCount(2, { timeout: 5_000 });
});
Expand Down Expand Up @@ -318,3 +395,153 @@
// After deletion only the add-cue-btn remains; no actual cue buttons should exist
await expect(page.locator('.cue-button:not(.add-cue-btn)')).not.toBeVisible({ timeout: 5_000 });
});

// ── Cue Groups ────────────────────────────────────────────────────────────

test('opens Add Cue modal with Individual Cue and Cue Group tabs', async () => {
// After individual cue tests all cues have been deleted. Still on the cue config page.
await page.locator('.add-cue-btn').first().click();
await waitForModal(page, 'Add Cue');
await expect(page.locator('.modal.show .nav-link:has-text("Individual Cue")')).toBeVisible();
await expect(page.locator('.modal.show .nav-link:has-text("Cue Group")')).toBeVisible();
// Dismiss via the × close button (Escape is unreliable in BVN with BTabs in Firefox)
await page.locator('.modal.show .btn-close').click();
await waitForModalClosed(page);
});

test('switches to Cue Group tab to show group form', async () => {
await page.locator('.add-cue-btn').first().click();
await waitForModal(page, 'Add Cue');
await page.locator('.modal.show .nav-link:has-text("Cue Group")').click();
// Wait for tab content to be visible (BTabs lazy may delay mount)
await expect(page.locator('.modal.show input[placeholder="1 > 100"]')).toBeVisible({
timeout: 10_000,
});
});

test('selects cue type and adds range 1 > 5', async () => {
// Scope to .tab-pane.active to avoid matching hidden Individual Cue tab's select.
// CueGroupForm filters out the N/A option, so LX is at index 0 (not 1).
await page.locator('.modal.show .tab-pane.active select').selectOption({ index: 0 });
await page.locator('.modal.show input[placeholder="1 > 100"]').fill('1 > 5');
await page.locator('.modal.show button:has-text("Add Range")').click();
// 5 cue ident inputs should appear (scoped to active tab pane to avoid hidden #new-cue-ident)
await expect(
page.locator('.modal.show .tab-pane.active input[placeholder="Identifier"]')
).toHaveCount(5, {
timeout: 3_000,
});
});

test('saves the cue group and shows a dashed group button with range label', async () => {
await page.locator('.modal.show button:has-text("Save Group")').click();
await waitForModalClosed(page);
await expect(page.locator('.cue-group-btn').first()).toBeVisible({ timeout: 5_000 });
await expect(page.locator('.cue-group-btn').first()).toContainText('LX 1 - LX 5');
});

test('clicks group button to open Edit Cue Group modal with 5 cues listed', async () => {
await page.locator('.cue-group-btn').first().click();
await waitForModal(page, 'Edit Cue Group');
await expect(page.locator('.modal.show input[placeholder="Identifier"]')).toHaveCount(5, {
timeout: 5_000,
});
});

test('adds label override and group button label updates on save', async () => {
await page.locator('.modal.show input[placeholder="e.g. Music Intro"]').fill('Music Intro');
await page.locator('.modal.show button:has-text("Save Group")').click();
await waitForModalClosed(page);
await expect(page.locator('.cue-group-btn').first()).toContainText('LX - Music Intro');
});

test('deletes the cue group and column returns to empty', async () => {
await page.locator('.cue-group-btn').first().click();
await waitForModal(page, 'Edit Cue Group');
await page.locator('.modal.show button:has-text("Delete Group")').click();
// useConfirm opens a stacked BVN modal — scope to its title to avoid intercepting the
// Edit Cue Group modal footer behind it.
await waitForModal(page, 'Delete Cue Group');
await page
.locator('.modal.show')
.filter({ has: page.locator('.modal-title:has-text("Delete Cue Group")') })
.locator('.modal-footer button.btn-danger')
.click();
await expect(page.locator('.cue-group-btn')).not.toBeVisible({ timeout: 5_000 });
});

// ── Line part removal ──────────────────────────────────────────────────────

test('remove button is absent on a single-part dialogue line', async () => {
await page.goto(`${UI_BASE}/show-config/script`);
await waitForAppReady(page);
await page.getByRole('button', { name: 'Edit', exact: true }).click();
await expect(page.locator('button:has-text("Stop Editing")')).toBeVisible({ timeout: 10_000 });

await page.click('button:has-text("Add Dialogue")');
const lineEditor = page
.locator('div.row')
.filter({ has: page.locator('button:has-text("Done")') })
.first();
await expect(lineEditor.locator('button:has-text("Done")')).toBeVisible({ timeout: 5_000 });

// With only 1 part no remove button should be present
await expect(lineEditor.locator('button.btn-outline-danger')).not.toBeVisible();
});

test('remove button appears when a second part is added', async () => {
const lineEditor = page
.locator('div.row')
.filter({ has: page.locator('button:has-text("Done")') })
.first();

// btn-secondary is the add-line-part (+) button — only btn-secondary in the editor row
await lineEditor.locator('button.btn-secondary').click();

// One remove button per part
await expect(lineEditor.locator('button.btn-outline-danger')).toHaveCount(2, { timeout: 3_000 });
});

test('clicking remove reduces to single part and hides the remove button', async () => {
const lineEditor = page
.locator('div.row')
.filter({ has: page.locator('button:has-text("Done")') })
.first();

await lineEditor.locator('button.btn-outline-danger').first().click();

// Back to 1 part — remove button gone
await expect(lineEditor.locator('button.btn-outline-danger')).not.toBeVisible({ timeout: 3_000 });
});

test('dialogue line with removed part saves correctly', async () => {
const lineEditor = page
.locator('div.row')
.filter({ has: page.locator('button:has-text("Done")') })
.first();

await lineEditor.locator('select').nth(0).selectOption({ label: 'Act 1' });
await lineEditor.locator('select').nth(1).selectOption({ label: 'Scene 1' });
await lineEditor
.locator('select')
.filter({ hasText: 'Hamlet' })
.selectOption({ label: 'Hamlet' });
await lineEditor.locator('input[type="text"]').last().fill('Surviving part text');

await lineEditor.locator('button:has-text("Done")').click();
// Delete the auto-added blank editor that appears after Done
await page.locator('button.btn-danger:has-text("Delete")').first().click();
await expect(page.locator('button:has-text("Done")')).not.toBeVisible({ timeout: 5_000 });

await page.getByRole('button', { name: 'Save', exact: true }).click();
await waitForModal(page, 'Saving Script');
await expect(page.locator('.modal.show').getByText('Finished saving script.')).toBeVisible({
timeout: 15_000,
});
await confirmModal(page);
await waitForModalClosed(page);

await expect(
page.locator('.viewable-line').filter({ hasText: 'Surviving part text' })
).toBeVisible({ timeout: 10_000 });
});
58 changes: 58 additions & 0 deletions client-v3/e2e/tests/13-live-show.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
UI_BASE,
waitForAppReady,
loginAsAdmin,
waitForModal,
confirmModal,
waitForModalClosed,
confirmDialog,
Expand Down Expand Up @@ -122,6 +123,63 @@
);
});

// ── Cue add mode in live view ─────────────────────────────────────────────

test('pressing C enables cue add mode showing + buttons on the live page', async () => {
await expect(leaderPage.locator('.script-item').first()).toBeVisible({ timeout: 10_000 });
await leaderPage.keyboard.press('C');
await expect(leaderPage.locator('.add-cue-btn').first()).toBeVisible({ timeout: 5_000 });
});

test('can add an individual cue from the live view', async () => {
await leaderPage.locator('.add-cue-btn').first().click();
await waitForModal(leaderPage, 'Add Cue');
await expect(
leaderPage.locator('.modal.show .nav-link:has-text("Individual Cue")')
).toBeVisible();
await leaderPage.locator('.modal.show #cue-type-input').selectOption({ index: 1 });
await leaderPage.locator('.modal.show #ident-input').fill('101');
await leaderPage.locator('.modal.show button:has-text("Add Cue")').click();
await waitForModalClosed(leaderPage);
await expect(leaderPage.locator('.cue-button:not(.add-cue-btn)').first()).toBeVisible({
timeout: 5_000,
});
});

test('can add a cue group from the live view', async () => {
await leaderPage.locator('.add-cue-btn').first().click();
await waitForModal(leaderPage, 'Add Cue');
await leaderPage.locator('.modal.show .nav-link:has-text("Cue Group")').click();
// Wait for CueGroupForm content (tab may be lazy in some BVN versions)
await expect(leaderPage.locator('.modal.show input[placeholder="1 > 100"]')).toBeVisible({
timeout: 10_000,
});
// CueGroupForm filters out N/A, so LX is at index 0
await leaderPage.locator('.modal.show .tab-pane.active select').selectOption({ index: 0 });
await leaderPage.locator('.modal.show input[placeholder="1 > 100"]').fill('200 > 202');
await leaderPage.locator('.modal.show button:has-text("Add Range")').click();
await expect(
leaderPage.locator('.modal.show .tab-pane.active input[placeholder="Identifier"]')
).toHaveCount(3, { timeout: 3_000 });
await leaderPage.locator('.modal.show button:has-text("Save Group")').click();
await waitForModalClosed(leaderPage);
await expect(leaderPage.locator('.cue-group-btn').first()).toBeVisible({ timeout: 5_000 });
await expect(leaderPage.locator('.cue-group-btn').first()).toContainText('LX 200 - LX 202');
});

test('cue group buttons in live view are non-interactive display-only buttons', async () => {
// Group buttons in live view should NOT open an edit modal — editing is blocked server-side
// during live sessions. Verify clicking doesn't open any modal.
await leaderPage.locator('.cue-group-btn').first().click();
await leaderPage.waitForTimeout(500);
await expect(leaderPage.locator('.modal.show')).not.toBeVisible();
});

test('pressing C again disables cue add mode', async () => {
await leaderPage.keyboard.press('C');
await expect(leaderPage.locator('.add-cue-btn').first()).not.toBeVisible({ timeout: 5_000 });
});

// ── Enable/disable Stage Manager mode ─────────────────────────────────────

test('can toggle Stage Manager mode', async () => {
Expand All @@ -140,7 +198,7 @@

// ── Reload Clients ─────────────────────────────────────────────────────────

test('Reload Clients triggers and all clients reload', async () => {

Check failure on line 201 in client-v3/e2e/tests/13-live-show.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ73DMzRuH9Pu861zbA1&open=AZ73DMzRuH9Pu861zbA1&pullRequest=1242
await leaderPage.locator('text=Live Config').click();
await leaderPage.click('button:has-text("Reload Clients")');
await confirmDialog(leaderPage);
Expand Down
Loading
Loading