diff --git a/tests/e2e/ccli-bookmarklet.spec.ts b/tests/e2e/ccli-bookmarklet.spec.ts new file mode 100644 index 0000000..6220919 --- /dev/null +++ b/tests/e2e/ccli-bookmarklet.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; + +test.describe('CCLI Bookmarklet', () => { + test('Settings page shows CCLI section with bookmarklet drag link', async ({ page }) => { + await page.goto('/settings'); + await page.waitForLoadState('networkidle'); + + const ccliBtn = page.locator('[data-testid="settings-submenu-ccli"]').first(); + await ccliBtn.waitFor({ state: 'visible', timeout: 10000 }); + await ccliBtn.click(); + await page.waitForTimeout(500); + + await expect(page.getByTestId('default-translation-language')).toBeVisible(); + await expect(page.getByTestId('ccli-bookmarklet-drag-link')).toBeVisible(); + }); + + test('bookmarklet endpoint returns valid JS', async ({ request }) => { + const res = await request.get('/bookmarklets/ccli-import.js'); + expect(res.status()).toBe(200); + expect(res.headers()['content-type']).toContain('text/javascript'); + + const body = await res.text(); + expect(body).toMatch(/^javascript:/); + expect(body).toContain('import-from-ccli-paste'); + expect(body).toContain('songselect.ccli.com'); + }); + + test('bookmarklet redirect page renders with valid base64 prefill', async ({ page }) => { + const payload = JSON.stringify({ + title: 'Amazing Grace', + author: 'John Newton', + ccliId: '4760', + sourceUrl: 'https://songselect.ccli.com/Songs/4760', + rawText: 'Amazing Grace\nJohn Newton\n\nVerse 1\nAmazing grace how sweet the sound\n\n© 1779\nCCLI # 4760', + }); + const encoded = Buffer.from(payload).toString('base64'); + + await page.goto(`/songs/import-from-ccli-paste?prefill=${encoded}`); + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('ccli-paste-textarea')).toBeVisible(); + }); + + test('bookmarklet redirect page shows error for invalid base64', async ({ page }) => { + await page.goto('/songs/import-from-ccli-paste?prefill=NOT_VALID_BASE64!!!'); + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('ccli-prefill-error-message')).toBeVisible(); + await expect(page.getByTestId('ccli-paste-textarea')).toBeVisible(); + }); + + test('default language dropdown persists selection', async ({ page }) => { + await page.goto('/settings'); + await page.waitForLoadState('networkidle'); + + const ccliBtn = page.locator('[data-testid="settings-submenu-ccli"]').first(); + await ccliBtn.waitFor({ state: 'visible', timeout: 10000 }); + await ccliBtn.click(); + await page.waitForTimeout(500); + + const select = page.getByTestId('default-translation-language'); + await select.selectOption('EN'); + await page.waitForTimeout(1000); + + await page.reload(); + await page.waitForLoadState('networkidle'); + const ccliBtn2 = page.locator('[data-testid="settings-submenu-ccli"]').first(); + await ccliBtn2.waitFor({ state: 'visible', timeout: 10000 }); + await ccliBtn2.click(); + await page.waitForTimeout(500); + + await expect(page.getByTestId('default-translation-language')).toHaveValue('EN'); + + await page.getByTestId('default-translation-language').selectOption('DE'); + await page.waitForTimeout(500); + }); +}); diff --git a/tests/e2e/ccli-paste-import.spec.ts b/tests/e2e/ccli-paste-import.spec.ts new file mode 100644 index 0000000..999caf8 --- /dev/null +++ b/tests/e2e/ccli-paste-import.spec.ts @@ -0,0 +1,113 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; + +function loadFixture(name: string): string { + return fs.readFileSync(`tests/Fixtures/ccli/${name}`, 'utf-8'); +} + +test.describe('CCLI Paste Import - SongDB', () => { + test('SongDB page shows CCLI import buttons', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('open-ccli-paste-dialog-button-songdb')).toBeVisible(); + }); + + test('SongSelect search button opens new tab with prefilled URL', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + + // Fill search input + await page.getByTestId('song-list-search-input').fill('amazing grace'); + await page.waitForTimeout(300); + + // Click SongSelect search button + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.getByTestId('songselect-search-button-songdb').click(), + ]); + + await popup.waitForLoadState('domcontentloaded'); + expect(popup.url()).toContain('songselect.ccli.com'); + expect(popup.url()).toContain('amazing'); + await popup.close(); + }); + + test('opens CCLI paste dialog from SongDB', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('open-ccli-paste-dialog-button-songdb').click(); + await expect(page.getByTestId('ccli-paste-textarea')).toBeVisible(); + await expect(page.getByTestId('ccli-preview-button')).toBeDisabled(); + }); + + test('preview button disabled when textarea empty', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('open-ccli-paste-dialog-button-songdb').click(); + await expect(page.getByTestId('ccli-preview-button')).toBeDisabled(); + }); + + test('paste fixture and preview shows metadata', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('open-ccli-paste-dialog-button-songdb').click(); + + const content = loadFixture('english-only-multi-verse.txt'); + await page.getByTestId('ccli-paste-textarea').fill(content); + await expect(page.getByTestId('ccli-preview-button')).toBeEnabled(); + + await page.getByTestId('ccli-preview-button').click(); + await page.waitForTimeout(3000); + + const errorMsg = page.getByTestId('ccli-error-message'); + const hasError = await errorMsg.isVisible().catch(() => false); + if (hasError) { + test.skip(); + return; + } + + await expect(page.locator('text=CCLI-Nr')).toBeVisible({ timeout: 5000 }); + }); + + test('duplicate import shows error with edit link', async ({ page }) => { + const content = loadFixture('english-only-multi-verse.txt'); + + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + const cookies = await page.context().cookies(); + const xsrf = cookies.find(c => c.name === 'XSRF-TOKEN')?.value ?? ''; + + const importRes = await page.request.post('/api/songs/import-from-ccli-paste', { + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': decodeURIComponent(xsrf), + }, + data: { raw_text: content, mode: 'create' }, + }); + if (importRes.status() !== 201) { + test.skip(); + return; + } + + // Now try to import again via UI + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + await page.getByTestId('open-ccli-paste-dialog-button-songdb').click(); + await page.getByTestId('ccli-paste-textarea').fill(content); + await page.getByTestId('ccli-preview-button').click(); + await page.waitForTimeout(2000); + + // Click import + const importBtn = page.getByTestId('ccli-import-stay-button'); + if (await importBtn.isVisible()) { + await importBtn.click(); + await page.waitForTimeout(2000); + await expect(page.getByTestId('ccli-error-message')).toBeVisible(); + await expect(page.getByTestId('ccli-existing-song-link')).toBeVisible(); + } + }); +}); diff --git a/tests/e2e/ccli-translation-pairing.spec.ts b/tests/e2e/ccli-translation-pairing.spec.ts new file mode 100644 index 0000000..20a566c --- /dev/null +++ b/tests/e2e/ccli-translation-pairing.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; + +test.describe('CCLI Translation Pairing', () => { + test('Translate.vue shows prefill banner when arrived from CCLI pairing', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + const cookies = await page.context().cookies(); + const xsrf = cookies.find(c => c.name === 'XSRF-TOKEN')?.value ?? ''; + + const content = fs.readFileSync('tests/Fixtures/ccli/english-only-multi-verse.txt', 'utf-8'); + const importRes = await page.request.post('/api/songs/import-from-ccli-paste', { + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': decodeURIComponent(xsrf), + }, + data: { raw_text: content, mode: 'create' }, + }); + + if (importRes.status() !== 201) { + test.skip(); + return; + } + + const { song_id } = await importRes.json(); + + await page.goto(`/songs/${song_id}/translate`); + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('translate-source-textarea')).toBeVisible(); + }); + + test('Translate.vue shows no prefill banner without prefill param', async ({ page }) => { + await page.goto('/songs'); + await page.waitForLoadState('networkidle'); + const cookies = await page.context().cookies(); + const xsrf = cookies.find(c => c.name === 'XSRF-TOKEN')?.value ?? ''; + + const content = fs.readFileSync('tests/Fixtures/ccli/english-only-multi-verse.txt', 'utf-8'); + const importRes = await page.request.post('/api/songs/import-from-ccli-paste', { + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': decodeURIComponent(xsrf), + }, + data: { raw_text: content, mode: 'create' }, + }); + + if (importRes.status() !== 201) { + test.skip(); + return; + } + + const { song_id } = await importRes.json(); + + await page.goto(`/songs/${song_id}/translate`); + await page.waitForLoadState('networkidle'); + + const banner = page.getByTestId('ccli-prefill-banner'); + await expect(banner).not.toBeVisible(); + }); +});