diff --git a/reports/bridge-introduction-layout-20260920/86-1280.png b/reports/bridge-introduction-layout-20260920/86-1280.png
new file mode 100644
index 0000000..b8eaf67
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/86-1280.png differ
diff --git a/reports/bridge-introduction-layout-20260920/86-1366.png b/reports/bridge-introduction-layout-20260920/86-1366.png
new file mode 100644
index 0000000..d5c3f2b
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/86-1366.png differ
diff --git a/reports/bridge-introduction-layout-20260920/86-desktop.png b/reports/bridge-introduction-layout-20260920/86-desktop.png
new file mode 100644
index 0000000..2395f74
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/86-desktop.png differ
diff --git a/reports/bridge-introduction-layout-20260920/86-lecture.png b/reports/bridge-introduction-layout-20260920/86-lecture.png
new file mode 100644
index 0000000..b2ae65b
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/86-lecture.png differ
diff --git a/reports/bridge-introduction-layout-20260920/87-1280.png b/reports/bridge-introduction-layout-20260920/87-1280.png
new file mode 100644
index 0000000..4934437
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/87-1280.png differ
diff --git a/reports/bridge-introduction-layout-20260920/87-1366.png b/reports/bridge-introduction-layout-20260920/87-1366.png
new file mode 100644
index 0000000..a53d19f
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/87-1366.png differ
diff --git a/reports/bridge-introduction-layout-20260920/87-desktop.png b/reports/bridge-introduction-layout-20260920/87-desktop.png
new file mode 100644
index 0000000..e03279c
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/87-desktop.png differ
diff --git a/reports/bridge-introduction-layout-20260920/87-lecture.png b/reports/bridge-introduction-layout-20260920/87-lecture.png
new file mode 100644
index 0000000..c265e99
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/87-lecture.png differ
diff --git a/reports/bridge-introduction-layout-20260920/88-1280.png b/reports/bridge-introduction-layout-20260920/88-1280.png
new file mode 100644
index 0000000..ae5cb8f
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/88-1280.png differ
diff --git a/reports/bridge-introduction-layout-20260920/88-1366.png b/reports/bridge-introduction-layout-20260920/88-1366.png
new file mode 100644
index 0000000..6e5450d
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/88-1366.png differ
diff --git a/reports/bridge-introduction-layout-20260920/88-desktop.png b/reports/bridge-introduction-layout-20260920/88-desktop.png
new file mode 100644
index 0000000..dfead4c
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/88-desktop.png differ
diff --git a/reports/bridge-introduction-layout-20260920/89-1280.png b/reports/bridge-introduction-layout-20260920/89-1280.png
new file mode 100644
index 0000000..3842ced
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/89-1280.png differ
diff --git a/reports/bridge-introduction-layout-20260920/89-1366.png b/reports/bridge-introduction-layout-20260920/89-1366.png
new file mode 100644
index 0000000..bf96e64
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/89-1366.png differ
diff --git a/reports/bridge-introduction-layout-20260920/89-desktop.png b/reports/bridge-introduction-layout-20260920/89-desktop.png
new file mode 100644
index 0000000..ec10538
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/89-desktop.png differ
diff --git a/reports/bridge-introduction-layout-20260920/index.html b/reports/bridge-introduction-layout-20260920/index.html
new file mode 100644
index 0000000..c12d833
--- /dev/null
+++ b/reports/bridge-introduction-layout-20260920/index.html
@@ -0,0 +1 @@
+
背景讲解布局验收背景讲解布局验收
四个任务通过:统一滚动、图文分栏、小屏布局、翻页与跳过;操作进度保持不变。未写入任务数据。
\ No newline at end of file
diff --git a/reports/bridge-introduction-layout-20260920/result.json b/reports/bridge-introduction-layout-20260920/result.json
new file mode 100644
index 0000000..2d03285
--- /dev/null
+++ b/reports/bridge-introduction-layout-20260920/result.json
@@ -0,0 +1,112 @@
+{
+ "pass": true,
+ "checks": [
+ {
+ "id": 86,
+ "viewport": {
+ "width": 1366,
+ "height": 768
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 86,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 87,
+ "viewport": {
+ "width": 1366,
+ "height": 768
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 87,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 88,
+ "viewport": {
+ "width": 1366,
+ "height": 768
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 88,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 89,
+ "viewport": {
+ "width": 1366,
+ "height": 768
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "id": 89,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "scrollable": true,
+ "introMoves": true,
+ "stepsOverflow": "visible",
+ "horizontalOverflow": false,
+ "imageTextOverlap": false
+ },
+ {
+ "optionalTextAndVideo": true,
+ "videoStopsOnSkip": true
+ },
+ {
+ "lanGatewayProxy": true
+ }
+ ],
+ "errors": [],
+ "writes": [],
+ "accountType": "internal maintenance preview",
+ "testDataCreated": false
+}
\ No newline at end of file
diff --git a/reports/bridge-introduction-layout-20260920/text-only.png b/reports/bridge-introduction-layout-20260920/text-only.png
new file mode 100644
index 0000000..3c60f4f
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/text-only.png differ
diff --git a/reports/bridge-introduction-layout-20260920/theme-dark.png b/reports/bridge-introduction-layout-20260920/theme-dark.png
new file mode 100644
index 0000000..30c21fd
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/theme-dark.png differ
diff --git a/reports/bridge-introduction-layout-20260920/theme-military.png b/reports/bridge-introduction-layout-20260920/theme-military.png
new file mode 100644
index 0000000..6cc1a9f
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/theme-military.png differ
diff --git a/reports/bridge-introduction-layout-20260920/video.png b/reports/bridge-introduction-layout-20260920/video.png
new file mode 100644
index 0000000..33495a6
Binary files /dev/null and b/reports/bridge-introduction-layout-20260920/video.png differ
diff --git a/tools/bridge-introduction-layout-live.mjs b/tools/bridge-introduction-layout-live.mjs
new file mode 100644
index 0000000..d7effe7
--- /dev/null
+++ b/tools/bridge-introduction-layout-live.mjs
@@ -0,0 +1,114 @@
+import fs from 'node:fs';
+import assert from 'node:assert/strict';
+import {chromium, expect} from '@playwright/test';
+import {login} from './role-permissions-live.mjs';
+import videoManifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js';
+
+const dir='reports/bridge-introduction-layout-20260920';
+fs.mkdirSync(dir,{recursive:true});
+const signed=await login('root'), checks=[], errors=[], writes=[];
+const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
+const page=await browser.newPage({viewport:{width:1920,height:1080}});
+await page.addInitScript(s=>{
+ sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);
+ sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);
+ localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'light'}));
+},signed);
+await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
+ const response=await route.fetch();
+ await route.fulfill({response,body:await response.text()+'\nglobalThis.introLayoutState=()=>activeTraining?{step:activeTraining.session.stepIndex,completed:[...activeTraining.session.completed]}:null;'});
+});
+page.on('pageerror',e=>errors.push(e.message));
+page.on('request',r=>{if(r.url().includes('/api/tran/')&&!['GET','HEAD'].includes(r.method()))writes.push(new URL(r.url()).pathname);});
+const state=()=>page.evaluate(()=>globalThis.introLayoutState?.());
+const overlay=page.locator('.training-introduction');
+try {
+ for(const [id,pages,steps] of [[86,6,5],[87,3,7],[88,1,11],[89,1,16]]) {
+ await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${id}`);
+ await expect(overlay).toBeVisible({timeout:120000});
+ await expect(page.locator('[data-intro-page]')).toHaveCount(pages);
+ await expect(page.locator('[data-step-index]')).toHaveCount(steps);
+ await expect(page.locator('[data-role="step-count"]')).toHaveText(`1 / ${pages+steps}`);
+ await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0)),{timeout:30000}).toBe(true);
+ const original=await state(), writeCount=writes.length;
+ await page.screenshot({path:`${dir}/${id}-desktop.png`});
+ for(const viewport of [{width:1366,height:768},{width:1280,height:720}]) {
+ await page.setViewportSize(viewport);
+ const layout=await page.evaluate(()=>{
+ const scroller=document.querySelector('.training-sequence-scroll');
+ const root=document.querySelector('[data-role="steps"]');
+ const content=document.querySelector('.training-introduction-content');
+ const image=document.querySelector('.introduction-visual'),text=document.querySelector('.introduction-reading');
+ const before=scroller.querySelector('[data-intro-page]').getBoundingClientRect().top;
+ scroller.scrollTop=scroller.scrollHeight;
+ return {scrollable:scroller.scrollHeight>scroller.clientHeight,
+ introMoves:scroller.querySelector('[data-intro-page]').getBoundingClientRect().topcontent.clientWidth,
+ imageTextOverlap:image&&text?image.getBoundingClientRect().right>text.getBoundingClientRect().left:false};
+ });
+ assert.ok(layout.scrollable);assert.ok(layout.introMoves);
+ assert.equal(layout.stepsOverflow,'visible');assert.equal(layout.horizontalOverflow,false);assert.equal(layout.imageTextOverlap,false);
+ await expect(page.locator('[data-step-index]').last()).toBeInViewport();
+ await expect(overlay.locator('[data-intro-command="skip"]')).toBeInViewport();
+ await expect(overlay.locator('[data-intro-command="next"]')).toBeInViewport();
+ await page.locator('.training-sequence-scroll').evaluate(el=>el.scrollTop=0);
+ await page.screenshot({path:`${dir}/${id}-${viewport.width}.png`});
+ checks.push({id,viewport,...layout});
+ }
+ for(let i=1;i1)await page.screenshot({path:`${dir}/${id}-lecture.png`});
+ await overlay.locator('[data-intro-command="next"]').click();
+ await expect(overlay).toBeHidden();
+ assert.deepEqual(await state(),original);
+ await expect(page.locator('[data-step-index="0"]')).toBeInViewport();
+ await page.locator('[data-intro-page="0"]').click();
+ await expect(overlay).toBeVisible();
+ await overlay.locator('[data-intro-command="skip"]').click();
+ await expect(overlay).toBeHidden();assert.deepEqual(await state(),original);
+ assert.equal(writes.length,writeCount);
+ await page.setViewportSize({width:1920,height:1080});
+ console.log('PASS task',id);
+ }
+ // Browser-only response fixture validates optional media without changing any saved task.
+ await page.route(/\/api\/tran\/v1\/teaching\/assignments\/86$/,async route=>{
+ const response=await route.fetch(),body=await response.json();
+ body.data.definitionSnapshot.introduction={background:{enabled:true,title:'纯文字背景',body:'没有背景图时,正文保持居中阅读。\n讲解切换不推进操作步骤。'},lectures:[{id:'video-check',title:'视频讲解',format:'video',videoUrl:videoManifest.scripts[0].steps[0].url}]};
+ await route.fulfill({response,json:body});
+ });
+ await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');
+ await expect(overlay).toBeVisible({timeout:120000});
+ await expect(overlay.locator('.is-text-only')).toBeVisible();
+ await expect(overlay.locator('.introduction-reading p')).toHaveCount(2);
+ await page.screenshot({path:`${dir}/text-only.png`});
+ await overlay.locator('[data-intro-command="next"]').click();
+ const video=overlay.locator('video');await expect(video).toBeVisible();
+ await video.evaluate(el=>el.play());
+ await expect.poll(()=>video.evaluate(el=>el.currentTime),{timeout:30000}).toBeGreaterThan(0);
+ await page.screenshot({path:`${dir}/video.png`});
+ await page.evaluate(()=>globalThis.previousIntroVideo=document.querySelector('.training-introduction video'));
+ await overlay.locator('[data-intro-command="skip"]').click();
+ assert.equal(await page.evaluate(()=>globalThis.previousIntroVideo.paused&&!globalThis.previousIntroVideo.getAttribute('src')),true);
+ checks.push({optionalTextAndVideo:true,videoStopsOnSkip:true});
+ await page.unroute(/\/api\/tran\/v1\/teaching\/assignments\/86$/);
+ await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');
+ await expect(overlay).toBeVisible({timeout:120000});
+ await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0))).toBe(true);
+ for(const mode of ['dark','military']) {
+ await page.evaluate(mode=>{document.documentElement.classList.toggle('dark',mode==='dark');document.documentElement.dataset.colorTheme=mode;document.documentElement.dataset.theme=mode==='dark'?'dark':'light';},mode);
+ await page.screenshot({path:`${dir}/theme-${mode}.png`});
+ }
+ const lan=await page.request.get('http://192.168.31.168:6180/api/auth/v1/system-config/public');
+ assert.equal(lan.status(),200);checks.push({lanGatewayProxy:true});
+ assert.deepEqual(errors,[]);
+ fs.writeFileSync(`${dir}/result.json`,JSON.stringify({pass:true,checks,errors,writes,accountType:'internal maintenance preview',testDataCreated:false},null,2));
+ fs.writeFileSync(`${dir}/index.html`,`背景讲解布局验收背景讲解布局验收
四个任务通过:统一滚动、图文分栏、小屏布局、翻页与跳过;操作进度保持不变。未写入任务数据。
${[86,87,88,89].map((id,i)=>``).join('')}`);
+} catch(error) {
+ await page.screenshot({path:`${dir}/failure.png`});
+ fs.writeFileSync(`${dir}/failure.json`,JSON.stringify({error:error.stack,errors},null,2));
+ throw error;
+} finally {await browser.close();}