dfg_3dviewer_js_library/tests/viewer.spec.ts
2026-06-25 09:11:23 +02:00

276 lines
9.4 KiB
TypeScript

// @ts-check
import { test, expect } from '@playwright/test';
const defaultModel = '/examples/box.stl';
const supportedFormatsText = 'GLB, GLTF, OBJ, DAE, FBX, PLY, IFC, STL, XYZ, JSON, 3DS, PCD';
const sandboxDropMessage = 'Drag and drop a 3D model into the viewer.';
const sandboxSupportedFormatsNotice = `<strong>Supported formats</strong>: ${supportedFormatsText}\n`;
const sandboxSupportedArchiveFormatsNotice = 'and <strong>archive formats</strong>: ZIP, RAR, TAR, XZ, GZ.';
const sandboxDropNotice = `${sandboxDropMessage} ${sandboxSupportedFormatsNotice} ${sandboxSupportedArchiveFormatsNotice}`;
const supportedExamples = [
{ format: 'dae', path: '/examples/box.dae' },
{ format: 'stl', path: '/examples/box.stl' },
{ format: 'ply', path: '/examples/box.ply' },
{ format: 'obj', path: '/examples/box.obj' },
{ format: 'xyz', path: '/examples/box.xyz' },
{ format: 'pcd', path: '/examples/box.pcd' },
{ format: '3ds', path: '/examples/box.3ds' },
{ format: 'ifc', path: '/examples/box.ifc' },
{ format: 'fbx', path: '/examples/box.fbx' },
{ format: 'glb', path: '/examples/box.glb' },
];
async function openViewer(page, modelPath = defaultModel) {
await page.addInitScript(() => {
window.__E2E__ = true;
});
await page.goto(`/?e2eModel=${encodeURIComponent(modelPath)}`);
await page.waitForSelector('#MainCanvas', { state: 'attached' });
}
async function openSandboxViewer(page) {
await page.addInitScript(() => {
window.__E2E__ = true;
});
await page.goto('/?sandbox=1');
await page.waitForSelector('#MainCanvas', { state: 'attached' });
}
async function waitForModel(page) {
await page.waitForFunction(() => window.viewer?.modelLoaded === true, {
timeout: 15_000,
});
}
async function waitForViewerIssue(page) {
await page.waitForFunction(
() =>
(window.viewer?.errors?.length ?? 0) > 0 ||
(window.viewer?.toasts?.length ?? 0) > 0,
{ timeout: 15_000 }
);
}
test('viewer runs in E2E mode', async ({ page }) => {
await openViewer(page);
const canvas = page.locator('#MainCanvas');
await expect(canvas).toBeVisible();
const hasWebGL = await canvas.evaluate((el) => {
const gl = el.getContext('webgl2') || el.getContext('webgl');
return !!gl;
});
expect(hasWebGL).toBe(true);
await expect.poll(() => page.evaluate(() => window.__E2E__)).toBe(true);
});
test('sandbox mode starts without loading a model', async ({ page }) => {
await openSandboxViewer(page);
await page.waitForFunction(
(msg) => window.viewer?.toasts?.some((t) => t.includes(msg)),
sandboxDropMessage
);
await page.waitForTimeout(3_000);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
toasts: window.viewer.toasts ?? [],
guiHidden: document.querySelector('#guiContainer')?.hidden,
sandboxNoticeVisible:
document.querySelector('#viewerStatusNotice[data-variant="sandbox"].is-visible')?.hidden === false,
noticeContainerCentered:
document.querySelector('#viewerNoticeContainer')?.classList.contains('viewer-notice-container--sandbox'),
}));
expect(state.modelLoaded).toBe(false);
expect(state.toasts.some((t) => t.includes(sandboxDropMessage))).toBe(true);
expect(state.guiHidden).toBe(true);
expect(state.sandboxNoticeVisible).toBe(true);
expect(state.noticeContainerCentered).toBe(true);
});
test('sandbox notice updates after language changes', async ({ page }) => {
await openSandboxViewer(page);
const notice = page.locator('#viewerStatusNotice[data-variant="sandbox"]');
await expect(notice.locator('.viewer-notice-message')).toHaveText(sandboxDropMessage);
// details are rendered as separate lines/spans: label, formats list, archives
await expect(notice.locator('.viewer-notice-detail').nth(0)).toContainText('Supported formats');
await expect(notice.locator('.viewer-notice-detail').nth(1)).toHaveText(supportedFormatsText);
await expect(notice.locator('.viewer-notice-detail').nth(2)).toContainText('archive formats');
await page.evaluate(() => {
document.querySelector<HTMLElement>('#viewerLanguageMode')?.click();
document.querySelector<HTMLElement>('.language-dropdown-item-polish')?.click();
});
await expect(notice.locator('.viewer-notice-message')).toHaveText("Przeciągnij i upuść model 3D w oknie viewer'a.");
await expect(notice.locator('.viewer-notice-detail').nth(0)).toContainText('formaty');
await expect(notice.locator('.viewer-notice-detail').nth(1)).toHaveText(supportedFormatsText);
await expect(notice.locator('.viewer-notice-detail').nth(2)).toContainText('archiwa');
});
for (const example of supportedExamples) {
test(`loads ${example.format.toUpperCase()} example into scene`, async ({ page }) => {
await openViewer(page, example.path);
await waitForModel(page);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
objectCount: window.viewer.scene.children.length,
}));
expect(state.modelLoaded).toBe(true);
expect(state.objectCount).toBeGreaterThan(0);
});
}
test('camera rotates on mouse drag', async ({ page }) => {
await openViewer(page);
await waitForModel(page);
await page.waitForFunction(() => window.viewer?.camera);
const canvas = page.locator('#MainCanvas');
const box = await canvas.boundingBox();
if (!box) {
throw new Error('MainCanvas bounding box is unavailable');
}
const before = await page.evaluate(() => ({
x: window.viewer.camera.position.x,
y: window.viewer.camera.position.y,
z: window.viewer.camera.position.z,
}));
const startX = box.x + box.width * 0.5;
const startY = box.y + box.height * 0.5;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(startX + 200, startY, { steps: 10 });
await page.mouse.up();
await expect
.poll(() =>
page.evaluate(() => ({
x: window.viewer.camera.position.x,
y: window.viewer.camera.position.y,
z: window.viewer.camera.position.z,
}))
)
.not.toEqual(before);
});
test('reports unsupported format without loading a model', async ({ page }) => {
await openViewer(page, '/examples/box.txt');
await waitForViewerIssue(page);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
errors: window.viewer.errors ?? [],
toasts: window.viewer.toasts ?? [],
}));
expect(state.modelLoaded).toBe(false);
expect(state.errors).toEqual([]);
expect(state.toasts).toContain('File extension is not supported yet.');
});
test('reports a missing model file instead of hanging', async ({ page }) => {
await openViewer(page, '/examples/does-not-exist.stl');
await waitForViewerIssue(page);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
errors: window.viewer.errors ?? [],
toasts: window.viewer.toasts ?? [],
}));
expect(state.modelLoaded).toBe(false);
expect(state.errors.length).toBeGreaterThan(0);
await expect
.poll(() => page.evaluate(() => window.viewer.errors.join(' ')))
.toContain('404');
});
test('loads OBJ even when the referenced MTL file is missing', async ({ page }) => {
await openViewer(page, '/examples/box-missing-mtl.obj');
await waitForModel(page);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
objectCount: window.viewer.scene.children.length,
toasts: window.viewer.toasts ?? [],
}));
expect(state.modelLoaded).toBe(true);
expect(state.objectCount).toBeGreaterThan(0);
expect(state.toasts).toContain('Error occurred while loading attached MTL file.');
});
test('reports a corrupted model file instead of hanging', async ({ page }) => {
await openViewer(page, '/examples/broken.glb');
await waitForViewerIssue(page);
const state = await page.evaluate(() => ({
modelLoaded: window.viewer.modelLoaded,
errors: window.viewer.errors ?? [],
toasts: window.viewer.toasts ?? [],
}));
expect(state.modelLoaded).toBe(false);
expect(state.errors.length).toBeGreaterThan(0);
});
test('loads config from drupalSettings when present', async ({ page }) => {
await page.addInitScript(() => {
window.__E2E__ = true;
window.drupalSettings = {
dfg_3dviewer: {
mainUrl: 'http://localhost',
baseNamespace: 'http://localhost',
metadataUrl: 'http://localhost',
baseModulePath: '/assets',
entity: {
bundle: 'test-bundle',
fieldDf: 'field_df',
idUri: '/wisski/navigate/(.*)/view',
viewEntityPath: '/wisski/navigate/',
attributeId: 'wisski_id',
exportViewer: 'field_df',
exportViewerUrl: '',
metadata: { source: 'Drupal', sourceType: '', url: '' },
},
viewer: {
container: 'DFG_3DViewer',
fileUpload: 'upload',
fileName: 'name',
imageGeneration: 'image',
lightweight: true,
editor: false,
scaleContainer: { x: '1', y: '1' },
gallery: { container: '', imageClass: '', imageId: '', build: false },
},
},
};
});
await page.goto(`/?e2eModel=${encodeURIComponent(defaultModel)}`);
await page.waitForSelector('#MainCanvas', { state: 'attached' });
const configSource = await page.evaluate(() => ({
bundle: window.viewer?.configBundle,
fromDrupal: window.viewer?.configFromDrupal,
}));
await page.waitForFunction(() => window.viewer?.configFromDrupal === true, {
timeout: 10_000,
});
expect(configSource.bundle).toBe('test-bundle');
});