fix: resolve workspace integration blockers (#5)

This commit is contained in:
2026-07-29 10:01:17 +09:00
parent 3279ac099e
commit cadbd5fb60
37 changed files with 1817 additions and 73 deletions
+53
View File
@@ -0,0 +1,53 @@
import { mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { expect, test } from '@playwright/test';
const evidenceDirectory = resolve('test-results/evidence');
test.beforeAll(() => {
mkdirSync(evidenceDirectory, { recursive: true });
});
test('2D DWG와 DXF 구동 증거를 캡처합니다', async ({ page }) => {
await page.goto('http://127.0.0.1:5173/');
await page.getByRole('button', { name: 'Sample DWG' }).click();
await expect(page.locator('#status')).toContainText('7309 entities');
await page.screenshot({
path: resolve(evidenceDirectory, 'viewer2d-dwg.png'),
fullPage: true,
});
await page.getByRole('button', { name: 'Sample DXF' }).click();
await expect(page.locator('#status')).toContainText('6 entities');
await page.screenshot({
path: resolve(evidenceDirectory, 'viewer2d-dxf.png'),
fullPage: true,
});
});
for (const [label, fixture, output] of [
['GLB', 'Box.glb', 'viewer3d-three-r185-glb.png'],
['PLY', 'DoublePrecision.ply', 'viewer3d-three-r185-ply.png'],
] as const) {
test(`Three.js r185 ${label} 구동 증거를 캡처합니다`, async ({ page }) => {
await page.goto(`http://127.0.0.1:3333/?model=/samples/${fixture}`);
await page.waitForFunction(
() =>
Boolean(
(
window as Window & {
__viewer?: { current?: object };
}
).__viewer?.current,
),
undefined,
{ timeout: 20_000 },
);
await expect(page.locator('canvas')).toHaveCount(1);
await page.screenshot({
path: resolve(evidenceDirectory, output),
fullPage: true,
});
});
}
+34
View File
@@ -0,0 +1,34 @@
import path from 'node:path';
import { expect, test } from '@playwright/test';
test('viewer2d loads DWG, DXF, and an uploaded file', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Sample DWG' }).click();
await expect(page.locator('#status')).toContainText('✓ BasicSample.dwg · 7309 entities');
await page.getByRole('button', { name: 'Layers' }).click();
await expect(page.locator('#layerList label').first()).toBeVisible();
await page.getByRole('button', { name: 'Sample DXF' }).click();
await expect(page.locator('#status')).toContainText('✓ simple.dxf · 6 entities');
await page.locator('#file').setInputFiles(
path.resolve('apps/viewer-2d-sample/public/samples/simple.dxf'),
);
await expect(page.locator('#status')).toContainText('✓ simple.dxf · 6 entities');
});
test('viewer2d rejects cross-origin and oversized CAD input', async ({ page }) => {
await page.goto('/?model=https://attacker.example/oversized.dxf');
await expect(page.locator('#status')).toContainText('같은 origin');
await page.goto('/');
await page.evaluate(() => {
const file = new File([new Uint8Array(50 * 1024 * 1024 + 1)], 'oversized.dxf');
const transfer = new DataTransfer();
transfer.items.add(file);
window.dispatchEvent(new DragEvent('drop', { dataTransfer: transfer }));
});
await expect(page.locator('#status')).toContainText('50 MiB');
});
+96
View File
@@ -0,0 +1,96 @@
import { expect, test } from '@playwright/test';
test('viewer2d releases its canvas when disposed', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Sample DXF' }).click();
await expect(page.locator('#status')).toContainText('✓ simple.dxf');
await expect(page.locator('canvas')).toHaveCount(1);
const lifecycle = await page.evaluate(() => {
const viewer = (
window as Window & {
__viewer2d?: { dispose?: () => void };
}
).__viewer2d;
return {
exposed: Boolean(viewer),
disposable: typeof viewer?.dispose === 'function',
};
});
expect(lifecycle).toEqual({ exposed: true, disposable: true });
await page.evaluate(() => {
(
window as Window & {
__viewer2d?: { dispose?: () => void };
}
).__viewer2d?.dispose?.();
});
await page.evaluate(() => {
(
window as Window & {
__viewer2d?: { dispose?: () => void };
}
).__viewer2d?.dispose?.();
});
await expect(page.locator('canvas')).toHaveCount(0);
});
test('viewer3d releases its canvas on pagehide and remains disposable', async ({ page }) => {
await page.goto('http://127.0.0.1:3333/?model=/samples/Box.glb');
await page.waitForFunction(
() =>
Boolean(
(
window as Window & {
__viewer?: { current?: object };
}
).__viewer?.current,
),
);
const result = await page.evaluate(() => {
window.dispatchEvent(new PageTransitionEvent('pagehide'));
const exposed = (window as Window & { __viewer?: { dispose: () => void } }).__viewer;
exposed?.dispose();
return {
canvases: document.querySelectorAll('canvas').length,
exposed: Boolean(exposed),
};
});
expect(result).toEqual({ canvases: 0, exposed: false });
});
test('viewer2d and viewer3d survive ten mount transitions without duplicate canvases', async ({
page,
}) => {
test.setTimeout(180_000);
const runtimeErrors: string[] = [];
page.on('pageerror', (error) => runtimeErrors.push(error.message));
for (let transition = 0; transition < 10; transition += 1) {
await page.goto('http://127.0.0.1:5173/');
await page.getByRole('button', { name: 'Sample DWG' }).click();
await expect(page.locator('#status')).toContainText('7309 entities', { timeout: 20_000 });
await expect(page.locator('canvas')).toHaveCount(1);
await page.goto('http://127.0.0.1:3333/?model=/samples/Box.glb');
await page.waitForFunction(
() =>
Boolean(
(
window as Window & {
__viewer?: { current?: object };
}
).__viewer?.current,
),
undefined,
{ timeout: 20_000 },
);
await expect(page.locator('canvas')).toHaveCount(1);
}
expect(runtimeErrors).toEqual([]);
});
+89
View File
@@ -0,0 +1,89 @@
import { expect, test } from '@playwright/test';
const fixtures = [
['GLB', 'Box.glb'],
['Draco GLB', 'Duck.optimized.glb'],
['OBJ', 'Cube.obj'],
['FBX', 'Cube.fbx'],
['DAE', 'Cube.dae'],
['IFC', 'Cube.ifc'],
['double PLY', 'DoublePrecision.ply'],
] as const;
for (const [label, fixture] of fixtures) {
test(`viewer3d renders ${label} with Three.js r185`, async ({ page }) => {
const runtimeErrors: string[] = [];
page.on('pageerror', (error) => runtimeErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') runtimeErrors.push(message.text());
});
await page.goto(`http://127.0.0.1:3333/?model=/samples/${fixture}`);
await page.waitForFunction(
() =>
Boolean(
(
window as Window & {
__viewer?: { current?: object };
}
).__viewer?.current,
),
undefined,
{ timeout: 20_000 },
);
const scene = await page.evaluate(() => {
const current = (
window as Window & {
__viewer?: {
current?: {
traverse: (visitor: (node: unknown) => void) => void;
};
};
}
).__viewer?.current;
let renderables = 0;
let vertices = 0;
let invalidValues = 0;
let float64Attributes = 0;
current?.traverse((node: unknown) => {
const geometry = (
node as {
geometry?: {
attributes?: {
position?: {
array: ArrayLike<number>;
count: number;
};
};
};
}
).geometry;
const position = geometry?.attributes?.position;
if (!position) return;
renderables += 1;
vertices += position.count;
if (position.array instanceof Float64Array) float64Attributes += 1;
for (let index = 0; index < position.array.length; index += 1) {
if (!Number.isFinite(position.array[index])) invalidValues += 1;
}
});
return {
canvases: document.querySelectorAll('canvas').length,
float64Attributes,
invalidValues,
renderables,
vertices,
};
});
expect(scene.canvases).toBe(1);
expect(scene.renderables).toBeGreaterThan(0);
expect(scene.vertices).toBeGreaterThan(0);
expect(scene.invalidValues).toBe(0);
expect(scene.float64Attributes).toBe(0);
expect(runtimeErrors).toEqual([]);
});
}
+30
View File
@@ -0,0 +1,30 @@
import { expect, test } from '@playwright/test';
test('viewer3d resolves Draco and IFC decoders under a production subpath', async ({ page }) => {
const failedRequests: string[] = [];
const responseUrls: string[] = [];
page.on('requestfailed', (request) => failedRequests.push(request.url()));
page.on('response', (response) => responseUrls.push(response.url()));
for (const fixture of ['Duck.optimized.glb', 'Cube.ifc']) {
await page.goto(
`http://127.0.0.1:43174/viewer-3d/?model=/viewer-3d/samples/${fixture}`,
);
await page.waitForFunction(
() =>
Boolean(
(
window as Window & {
__viewer?: { current?: object };
}
).__viewer?.current,
),
undefined,
{ timeout: 20_000 },
);
}
expect(responseUrls.some((url) => url.includes('/viewer-3d/draco/'))).toBe(true);
expect(responseUrls.some((url) => url.endsWith('/viewer-3d/web-ifc/web-ifc.wasm'))).toBe(true);
expect(failedRequests).toEqual([]);
});