Expand integration coverage and fix settings UI
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Browser Tests
|
||||
|
||||
These JF12-only integration tests create three temporary NFO movie fixtures,
|
||||
seed deterministic Multilang data for their real Jellyfin IDs, and exercise
|
||||
the ordinary web UI with Chromium. They cover proxy translation/fallback and
|
||||
ordering, genre translation, per-user enablement, pre-cache population,
|
||||
category-rule precedence, and both settings pages. The runner restores the
|
||||
complete JF12 `config`, `data`, and `cache` volumes afterward. It refuses any target except
|
||||
`jellyfin12` on port `8097`.
|
||||
|
||||
Install the ignored local dependencies once:
|
||||
|
||||
```bash
|
||||
cd tests/browser
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Run from the repository root with JF12 test-account credentials:
|
||||
|
||||
```bash
|
||||
python3 tools/run_jellyfin12_browser_tests.py \
|
||||
--base-url http://server.sedomain:8097 \
|
||||
--username "$JELLYFIN_TEST_USER" \
|
||||
--password "$JELLYFIN_TEST_PASSWORD" \
|
||||
--ssh-host server.sedomain \
|
||||
--state-root /mnt/apps-pool/Docker/Volumes/jellyfin12 \
|
||||
--confirm-destructive
|
||||
```
|
||||
|
||||
`node_modules`, browser artifacts, credentials, test results, and traces are
|
||||
ignored. A failed run still restores the server state in a `finally` block; a
|
||||
restoration hash mismatch intentionally leaves its remote snapshot in `/tmp`
|
||||
for investigation.
|
||||
@@ -0,0 +1,218 @@
|
||||
const fs = require('node:fs');
|
||||
const { expect, test } = require('@playwright/test');
|
||||
|
||||
const fixturePath = process.env.MULTILANG_BROWSER_FIXTURE;
|
||||
if (!fixturePath) {
|
||||
throw new Error('MULTILANG_BROWSER_FIXTURE must point to the temporary JF12 fixture description.');
|
||||
}
|
||||
|
||||
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
||||
const actionFields = ['title', 'overview', 'tagline', 'poster', 'logo', 'banner', 'thumb', 'backdrop'];
|
||||
|
||||
function authHeaders() {
|
||||
return { Authorization: `MediaBrowser Token="${fixture.token}"` };
|
||||
}
|
||||
|
||||
function jellyfinActions() {
|
||||
return Object.fromEntries(actionFields.map(field => [field, ['Jellyfin']]));
|
||||
}
|
||||
|
||||
async function apiGet(request, path) {
|
||||
const response = await request.get(path, { headers: authHeaders() });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function apiPost(request, path, data) {
|
||||
const response = await request.post(path, {
|
||||
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
data
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function restoreFixtureRules(request) {
|
||||
const config = await apiGet(request, '/Multilang/AdminConfig');
|
||||
Object.assign(config, {
|
||||
Languages: ['en', 'fi'],
|
||||
ItemsProxyCacheThresholdMs: 0,
|
||||
ItemsProxyCacheTtlMinutes: 5,
|
||||
ItemsProxyCacheMaxMiB: 8,
|
||||
PrecacheMovieLibraries: true,
|
||||
PrecacheTvShowLibraries: false
|
||||
});
|
||||
await apiPost(request, '/Multilang/AdminConfig', config);
|
||||
|
||||
const rules = await apiGet(request, '/Multilang/UserRules/self');
|
||||
Object.assign(rules, {
|
||||
Enabled: true,
|
||||
SortLocale: 'fi-FI',
|
||||
Categories: [],
|
||||
FallbackFieldActions: {
|
||||
...jellyfinActions(),
|
||||
title: ['Language:fi', 'Jellyfin'],
|
||||
overview: ['Language:fi', 'Jellyfin'],
|
||||
tagline: ['Language:fi', 'Jellyfin']
|
||||
}
|
||||
});
|
||||
await apiPost(request, '/Multilang/UserRules/self', rules);
|
||||
await apiPost(request, '/Multilang/ClearCache', {});
|
||||
}
|
||||
|
||||
async function signIn(page) {
|
||||
await page.goto('/web/index.html');
|
||||
await page.getByRole('textbox', { name: 'User' }).fill(fixture.username);
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(fixture.password);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
await expect(page).not.toHaveURL(/login/i);
|
||||
}
|
||||
|
||||
async function openFixtureLibrary(page, expectedTitle) {
|
||||
await page.goto(`/web/#/movies?topParentId=${fixture.libraryId}&collectionType=movies`);
|
||||
await expect(page.getByText(expectedTitle, { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async function getFixtureItems(request) {
|
||||
const upstream = `/Users/${fixture.userId}/Items?ParentId=${fixture.libraryId}&IncludeItemTypes=Movie&Recursive=true&SortBy=SortName&SortOrder=Ascending&Fields=Overview,Genres,GenreItems`;
|
||||
const response = await request.get('/Multilang/ItemsProxy', {
|
||||
headers: { Authorization: `MediaBrowser Token="${fixture.token}"` },
|
||||
params: { url: upstream, mlLocale: 'fi-FI' }
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await restoreFixtureRules(request);
|
||||
});
|
||||
|
||||
test('pre-caches the fixture movie library after user activity', async ({ request }) => {
|
||||
const trigger = await request.get('/Multilang/ItemsProxy', {
|
||||
headers: authHeaders(),
|
||||
params: {
|
||||
url: `/Users/${fixture.userId}/Items/Latest?Limit=1`,
|
||||
mlLocale: 'fi-FI'
|
||||
}
|
||||
});
|
||||
expect(trigger.ok()).toBeTruthy();
|
||||
|
||||
await expect.poll(async () => {
|
||||
const entries = await apiGet(request, '/Multilang/CacheEntries');
|
||||
return entries.some(entry => entry.Url.includes(`ParentId=${fixture.libraryId}`));
|
||||
}, { timeout: 60000 }).toBeTruthy();
|
||||
});
|
||||
|
||||
test('shows configured translations and uses the injected ItemsProxy route', async ({ page, request }) => {
|
||||
const proxiedRequests = [];
|
||||
page.on('request', request => {
|
||||
if (new URL(request.url()).pathname === '/Multilang/ItemsProxy') {
|
||||
proxiedRequests.push(request.url());
|
||||
}
|
||||
});
|
||||
|
||||
await signIn(page);
|
||||
await openFixtureLibrary(page, fixture.translatedTitles[0]);
|
||||
|
||||
for (const title of fixture.translatedTitles) {
|
||||
await expect(page.getByText(title, { exact: true })).toBeVisible();
|
||||
}
|
||||
const payload = await getFixtureItems(request);
|
||||
expect(payload.Items.map(item => item.Id)).toEqual(fixture.translatedOrderedItemIds);
|
||||
expect(payload.Items.map(item => item.Name)).toEqual(fixture.translatedSortedTitles);
|
||||
expect(payload.Items.find(item => item.Name === 'The Clock').Overview).toBe('English overview for The Clock.');
|
||||
expect(payload.Items.find(item => item.Name === 'Ankkuri').Genres).toEqual(['Draama']);
|
||||
expect(payload.Items.find(item => item.Name === 'Majakka').Genres).toEqual(['Komedia']);
|
||||
await expect.poll(() => proxiedRequests.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('returns to Jellyfin titles when Multilang is disabled for the user', async ({ page, request }) => {
|
||||
const rules = await apiGet(request, '/Multilang/UserRules/self');
|
||||
rules.Enabled = false;
|
||||
await apiPost(request, '/Multilang/UserRules/self', rules);
|
||||
|
||||
await signIn(page);
|
||||
await openFixtureLibrary(page, fixture.jellyfinTitles[0]);
|
||||
for (const title of fixture.jellyfinTitles) {
|
||||
await expect(page.getByText(title, { exact: true })).toBeVisible();
|
||||
}
|
||||
const payload = await getFixtureItems(request);
|
||||
expect(payload.Items.map(item => item.Id)).toEqual(fixture.orderedItemIds);
|
||||
expect(payload.Items.map(item => item.Name)).toEqual(fixture.jellyfinTitles);
|
||||
expect(payload.Items.find(item => item.Name === 'The Anchor').Genres).toEqual(['Drama']);
|
||||
expect(payload.Items.find(item => item.Name === 'The Beacon').Genres).toEqual(['Comedy']);
|
||||
});
|
||||
|
||||
test('applies a structured category rule before fallback translation actions', async ({ request }) => {
|
||||
const rules = await apiGet(request, '/Multilang/UserRules/self');
|
||||
const categoryActions = jellyfinActions();
|
||||
categoryActions.title = ['Jellyfin'];
|
||||
rules.Categories = [{
|
||||
Id: 'browser-fixture-swedish',
|
||||
Label: 'Swedish originals',
|
||||
Requirements: [{
|
||||
Field: 'original_language',
|
||||
Fields: ['original_language'],
|
||||
Relation: 'is',
|
||||
Values: ['sv'],
|
||||
UseOr: false,
|
||||
UseFieldOr: false
|
||||
}],
|
||||
MatchAllConditions: true,
|
||||
Scopes: ['M'],
|
||||
FieldActionLists: categoryActions,
|
||||
FieldActions: {}
|
||||
}];
|
||||
await apiPost(request, '/Multilang/UserRules/self', rules);
|
||||
|
||||
const payload = await getFixtureItems(request);
|
||||
expect(payload.Items.map(item => item.Name)).toEqual(['Ankkuri', 'The Beacon', 'The Clock']);
|
||||
});
|
||||
|
||||
test('saves admin configuration through the dashboard page', async ({ page, request }) => {
|
||||
await signIn(page);
|
||||
await page.goto('/web/#/configurationpage?name=Multilang');
|
||||
await expect(page.locator('#ml-admin')).toBeVisible();
|
||||
await page.locator('#ml-languages').fill('en, fi, sv');
|
||||
await page.locator('#ml-precache-movies').check();
|
||||
await page.locator('#ml-precache-tvshows').uncheck();
|
||||
await page.locator('#ml-save').click();
|
||||
await expect(page.locator('#ml-status')).toHaveText('Saved.');
|
||||
|
||||
const config = await apiGet(request, '/Multilang/AdminConfig');
|
||||
expect(config.Languages).toEqual(['en', 'fi', 'sv']);
|
||||
expect(config.PrecacheMovieLibraries).toBeTruthy();
|
||||
expect(config.PrecacheTvShowLibraries).toBeFalsy();
|
||||
});
|
||||
|
||||
test('saves a structured classification rule through the user settings page', async ({ page, request }) => {
|
||||
await signIn(page);
|
||||
await page.goto(`/web/#/mypreferences?userId=${fixture.userId}`);
|
||||
await page.getByRole('button', { name: 'User Menu' }).click();
|
||||
await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible();
|
||||
await page.locator('#mlUserRulesMenuItem').click();
|
||||
await expect(page.locator('#ml-user')).toBeVisible();
|
||||
await expect(page.locator('#ml-sort-locale option[value="en"]')).toHaveCount(1);
|
||||
await page.locator('#ml-sort-locale').selectOption('en');
|
||||
await page.getByRole('button', { name: 'Add classification rule' }).click();
|
||||
|
||||
const rule = page.locator('.ml-classification-rule').last();
|
||||
await rule.locator('[data-category-label]').fill('Browser fixture rule');
|
||||
await rule.locator('.ml-field-dropdown button').click();
|
||||
await rule.locator('.ml-field-dropdown input[value="original_language"]').check();
|
||||
await rule.locator('select.ml-relation-select').selectOption('is');
|
||||
await rule.locator('.ml-value-dropdown').nth(1).locator('button').click();
|
||||
await rule.locator('.ml-value-dropdown').nth(1).locator('input[value="sv"]').check();
|
||||
await page.locator('#ml-save-user').click();
|
||||
await expect(page.locator('#ml-user-status')).toHaveText('Saved.');
|
||||
|
||||
const rules = await apiGet(request, '/Multilang/UserRules/self');
|
||||
expect(rules.SortLocale).toBe('en');
|
||||
expect(rules.Categories).toHaveLength(1);
|
||||
expect(rules.Categories[0].Label).toBe('Browser fixture rule');
|
||||
expect(rules.Categories[0].Requirements).toEqual([expect.objectContaining({
|
||||
Fields: ['original_language'],
|
||||
Relation: 'is',
|
||||
Values: ['sv']
|
||||
})]);
|
||||
});
|
||||
Generated
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "jellyfin-multilang-browser-tests",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jellyfin-multilang-browser-tests",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "jellyfin-multilang-browser-tests",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:headed": "playwright test --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.61.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: '.',
|
||||
testMatch: 'multilang.spec.js',
|
||||
fullyParallel: false,
|
||||
retries: 0,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: process.env.JELLYFIN_BASE_URL,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure'
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user