test(e2e): add CCLI paste import, bookmarklet, and translation pairing e2e tests
This commit is contained in:
parent
f2b10a4cd7
commit
03fdfac3d3
77
tests/e2e/ccli-bookmarklet.spec.ts
Normal file
77
tests/e2e/ccli-bookmarklet.spec.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
113
tests/e2e/ccli-paste-import.spec.ts
Normal file
113
tests/e2e/ccli-paste-import.spec.ts
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
61
tests/e2e/ccli-translation-pairing.spec.ts
Normal file
61
tests/e2e/ccli-translation-pairing.spec.ts
Normal file
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue