test(e2e): add CCLI paste import, bookmarklet, and translation pairing e2e tests

This commit is contained in:
Thorsten Bus 2026-05-11 10:36:44 +02:00
parent f2b10a4cd7
commit 03fdfac3d3
3 changed files with 251 additions and 0 deletions

View 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);
});
});

View 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();
}
});
});

View 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();
});
});