Initial commit
This commit is contained in:
commit
05c65aad4d
155 changed files with 93617 additions and 0 deletions
276
tests/viewer.spec.ts
Normal file
276
tests/viewer.spec.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
// @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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue