138 خطوط
13 KiB
JavaScript
138 خطوط
13 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
|
|
const chromePath = process.env.CHROME_PATH ?? 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
|
const appUrl = process.env.REVIEW_APP_URL ?? 'http://127.0.0.1:5174'
|
|
const apiUrl = process.env.REVIEW_API_URL ?? 'http://127.0.0.1:8800'
|
|
const outputDirectory = resolve(process.cwd(), 'docs', 'visual-review')
|
|
const debuggingPort = 9223
|
|
const profile = join(tmpdir(), `microlearn-visual-${Date.now()}`)
|
|
const screenPrefix = process.env.REVIEW_SCREEN_PREFIX
|
|
const screenFiles = process.env.REVIEW_SCREEN_FILES?.split(',').map(value => value.trim()).filter(Boolean)
|
|
|
|
await mkdir(outputDirectory, { recursive: true })
|
|
const login = await fetch(`${apiUrl}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ email: 'designer@microlearn.test', password: 'password' }) })
|
|
if (!login.ok) throw new Error(`Review login failed with ${login.status}`)
|
|
const { data: session } = await login.json()
|
|
const learnerLogin = await fetch(`${apiUrl}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ email: 'maryam@microlearn.test', password: 'password' }) })
|
|
if (!learnerLogin.ok) throw new Error(`Learner review login failed with ${learnerLogin.status}`)
|
|
const { data: learnerSession } = await learnerLogin.json()
|
|
const managerLogin = await fetch(`${apiUrl}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ email: 'manager@microlearn.test', password: 'password' }) })
|
|
if (!managerLogin.ok) throw new Error(`Manager review login failed with ${managerLogin.status}`)
|
|
const { data: managerSession } = await managerLogin.json()
|
|
const learnerHome = await fetch(`${apiUrl}/api/v1/learner/home`, { headers: { Accept: 'application/json', Authorization: `Bearer ${learnerSession.token}` } }).then(response => response.json())
|
|
const learnerAssignment = learnerHome.data?.assigned?.[0]
|
|
const courseList = await fetch(`${apiUrl}/api/v1/courses?perPage=1`, { headers: { Accept: 'application/json', Authorization: `Bearer ${session.token}` } }).then(response => response.json())
|
|
const reviewCourseId = courseList.data?.[0]?.id
|
|
|
|
const chrome = spawn(chromePath, [`--headless=new`, `--remote-debugging-port=${debuggingPort}`, `--user-data-dir=${profile}`, '--disable-gpu', '--no-first-run', '--no-default-browser-check', 'about:blank'], { stdio: 'ignore' })
|
|
|
|
try {
|
|
const target = await waitForTarget()
|
|
const socket = new WebSocket(target.webSocketDebuggerUrl)
|
|
await new Promise((resolveOpen, reject) => { socket.addEventListener('open', resolveOpen, { once: true }); socket.addEventListener('error', reject, { once: true }) })
|
|
let sequence = 0
|
|
const pending = new Map()
|
|
socket.addEventListener('message', event => { const message = JSON.parse(event.data); if (!message.id) return; const request = pending.get(message.id); if (!request) return; pending.delete(message.id); message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result) })
|
|
const call = (method, params = {}) => new Promise((resolveCall, reject) => { const id = ++sequence; pending.set(id, { resolve: resolveCall, reject }); socket.send(JSON.stringify({ id, method, params })) })
|
|
await call('Page.enable')
|
|
await call('Runtime.enable')
|
|
await call('Page.navigate', { url: appUrl })
|
|
await delay(500)
|
|
await call('Runtime.evaluate', { expression: `localStorage.setItem('microlearn.access_token', ${JSON.stringify(session.token)})` })
|
|
|
|
const screens = [
|
|
{ file: 'phase3-users-desktop.png', path: '/app/users', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'phase3-teams-mobile-dark.png', path: '/app/teams', width: 390, height: 844, locale: 'fa', theme: 'dark' },
|
|
{ file: 'phase3-subscription-ltr.png', path: '/app/subscription', width: 1280, height: 900, locale: 'en', theme: 'light' },
|
|
{ file: 'phase4-courses-desktop.png', path: '/app/courses', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'phase4-new-course-mobile-dark.png', path: '/app/courses/new', width: 390, height: 844, locale: 'fa', theme: 'dark' },
|
|
...(reviewCourseId ? [{ file: 'phase4-workspace-ltr.png', path: `/app/courses/${reviewCourseId}?tab=content`, width: 1280, height: 900, locale: 'en', theme: 'light' }] : []),
|
|
{ file: 'phase5-builder-desktop-rtl.png', path: '/app/builder-preview', width: 1600, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'phase5-builder-tablet-dark.png', path: '/app/builder-preview', width: 1440, height: 1000, locale: 'fa', theme: 'dark', action: `[aria-label="tablet"]` },
|
|
{ file: 'phase5-builder-mobile-preview-ltr.png', path: '/app/builder-preview', width: 1440, height: 1000, locale: 'en', theme: 'light', action: `[aria-label="mobile"]` },
|
|
{ file: 'phase6-library-desktop-rtl.png', path: '/app/library', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'phase6-library-mobile-ltr-dark.png', path: '/app/library', width: 390, height: 844, locale: 'en', theme: 'dark' },
|
|
{ file: 'phase6-builder-navigator-ltr.png', path: '/app/builder-preview', width: 1440, height: 1000, locale: 'en', theme: 'light', actions: ['.block-library .panel-tabs button:nth-child(2)'] },
|
|
{ file: 'phase6-builder-accessibility-rtl-dark.png', path: '/app/builder-preview', width: 1440, height: 1000, locale: 'fa', theme: 'dark', actions: ['.canvas-block', '.inspector .panel-tabs button:nth-child(2)'] },
|
|
{ file: 'pre-phase7-library-cards-5cm.png', path: '/app/library', width: 1440, height: 1000, locale: 'fa', theme: 'light', action: '[aria-label="نمایش کارتی"]' },
|
|
{ file: 'pre-phase7-library-rows.png', path: '/app/library', width: 1440, height: 1000, locale: 'fa', theme: 'light', action: '[aria-label="نمایش ردیفی"]' },
|
|
{ file: 'pre-phase7-users-import.png', path: '/app/users', width: 1440, height: 1000, locale: 'fa', theme: 'light', action: '.page-actions .button--success' },
|
|
{ file: 'pre-phase7-teams-enhanced.png', path: '/app/teams', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'pre-phase7-courses-compact.png', path: '/app/courses', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
...(reviewCourseId ? [{ file: 'pre-phase7-course-cover.png', path: `/app/courses/${reviewCourseId}`, width: 1440, height: 1000, locale: 'fa', theme: 'light' }] : []),
|
|
{ file: 'phase7-question-bank-rtl.png', path: '/app/question-bank', width: 1440, height: 1000, locale: 'fa', theme: 'light' },
|
|
{ file: 'phase7-assessment-mapping-rtl.png', path: '/app/question-bank', width: 1440, height: 1000, locale: 'fa', theme: 'light', actions: ['.studio-tabs button:nth-child(2)', '.assessment-list > button:first-child', '.assessment-question-list .button--quiet'] },
|
|
{ file: 'phase7-scenarios-mobile-dark.png', path: '/app/question-bank', width: 390, height: 844, locale: 'fa', theme: 'dark', actions: ['.studio-tabs button:nth-child(3)'] },
|
|
{ file: 'phase9-learner-home-desktop-rtl.png', path: '/learn/home', width: 1280, height: 900, locale: 'fa', theme: 'light', token: learnerSession.token },
|
|
{ file: 'phase9-learner-home-mobile-dark.png', path: '/learn/home', width: 390, height: 844, locale: 'fa', theme: 'dark', token: learnerSession.token },
|
|
...(learnerAssignment ? [
|
|
{ file: 'phase9-player-desktop-rtl.png', path: `/learn/player/${learnerAssignment.id}?course=${learnerAssignment.courseVersionId}&lesson=${learnerAssignment.lessonId}`, width: 1280, height: 900, locale: 'fa', theme: 'light', token: learnerSession.token },
|
|
{ file: 'phase9-player-mobile-dark.png', path: `/learn/player/${learnerAssignment.id}?course=${learnerAssignment.courseVersionId}&lesson=${learnerAssignment.lessonId}`, width: 390, height: 844, locale: 'fa', theme: 'dark', token: learnerSession.token },
|
|
] : []),
|
|
{ file: 'phase10-manager-overview-desktop-rtl.png', path: '/manager/overview', width: 1440, height: 1000, locale: 'fa', theme: 'light', token: managerSession.token },
|
|
{ file: 'phase10-manager-overview-desktop-ltr.png', path: '/manager/overview', width: 1280, height: 900, locale: 'en', theme: 'light', token: managerSession.token },
|
|
{ file: 'phase10-manager-team-tablet-dark.png', path: '/manager/team', width: 900, height: 1000, locale: 'fa', theme: 'dark', token: managerSession.token },
|
|
{ file: 'phase10-manager-attention-mobile-rtl.png', path: '/manager/attention', width: 390, height: 844, locale: 'fa', theme: 'light', token: managerSession.token },
|
|
{ file: 'phase10-learner-home-ios-small.png', path: '/learn/home', width: 375, height: 812, locale: 'fa', theme: 'light', token: learnerSession.token },
|
|
{ file: 'phase10-learner-home-ios-desktop-ltr.png', path: '/learn/home', width: 1280, height: 900, locale: 'en', theme: 'light', token: learnerSession.token },
|
|
{ file: 'phase10-learner-more-ios-dark.png', path: '/learn/more', width: 430, height: 932, locale: 'fa', theme: 'dark', token: learnerSession.token },
|
|
...(learnerAssignment ? [{ file: 'phase10-learner-player-ios.png', path: `/learn/player/${learnerAssignment.id}?course=${learnerAssignment.courseVersionId}&lesson=${learnerAssignment.lessonId}`, width: 390, height: 844, locale: 'fa', theme: 'light', token: learnerSession.token }] : []),
|
|
{ file: 'phase11-monitoring-overview-desktop-rtl.png', path: '/app/monitoring', width: 1440, height: 1000, locale: 'fa', theme: 'light', wait: 5000 },
|
|
{ file: 'phase11-monitoring-assessments-tablet-dark.png', path: '/app/monitoring/assessments', width: 900, height: 1000, locale: 'fa', theme: 'dark', wait: 5000 },
|
|
{ file: 'phase11-monitoring-risk-mobile-rtl.png', path: '/app/monitoring/risk', width: 390, height: 844, locale: 'fa', theme: 'light', wait: 5000 },
|
|
{ file: 'phase11-monitoring-skills-375-dark.png', path: '/app/monitoring/skills', width: 375, height: 812, locale: 'en', theme: 'dark', wait: 5000 },
|
|
{ file: 'phase11-monitoring-overview-desktop-ltr.png', path: '/app/monitoring', width: 1280, height: 900, locale: 'en', theme: 'light', wait: 5000 },
|
|
]
|
|
|
|
for (const screen of screens.filter(screen => (!screenPrefix || screen.file.startsWith(screenPrefix)) && (!screenFiles?.length || screenFiles.includes(screen.file)))) {
|
|
await call('Emulation.setDeviceMetricsOverride', { width: screen.width, height: screen.height, deviceScaleFactor: 1, mobile: screen.width < 600 })
|
|
const initialization = await call('Page.addScriptToEvaluateOnNewDocument', { source: `localStorage.setItem('microlearn.access_token', ${JSON.stringify(screen.token ?? session.token)}); localStorage.setItem('microlearn.locale', '${screen.locale}'); localStorage.setItem('microlearn.theme', '${screen.theme}')` })
|
|
await call('Runtime.evaluate', { expression: `localStorage.setItem('microlearn.access_token', ${JSON.stringify(screen.token ?? session.token)}); localStorage.setItem('microlearn.locale', '${screen.locale}'); localStorage.setItem('microlearn.theme', '${screen.theme}')` })
|
|
await call('Page.navigate', { url: `${appUrl}${screen.path}` })
|
|
await delay(300)
|
|
await call('Page.reload', { ignoreCache: true })
|
|
await delay(screen.wait ?? (screen.token ? 5000 : 1600))
|
|
await call('Page.removeScriptToEvaluateOnNewDocument', { identifier: initialization.identifier })
|
|
if (screen.locale === 'en') {
|
|
await call('Runtime.evaluate', { expression: `if (document.documentElement.lang !== 'en') { const direct = document.querySelector('[aria-label="تغییر زبان"]'); if (direct) direct.click(); else document.querySelector('[aria-label="بازکردن حساب کاربری"]')?.click() }` })
|
|
await delay(350)
|
|
await call('Runtime.evaluate', { expression: `if (document.documentElement.lang !== 'en') document.querySelector('.learner-account-actions button:first-child')?.click()` })
|
|
await delay(350)
|
|
await call('Runtime.evaluate', { expression: `document.querySelector('[aria-label="Close"]')?.click()` })
|
|
await call('Runtime.evaluate', { expression: `document.documentElement.lang = 'en'; document.documentElement.dir = 'ltr'` })
|
|
}
|
|
if (screen.action) {
|
|
await call('Runtime.evaluate', { expression: `document.querySelector(${JSON.stringify(screen.action)})?.click()` })
|
|
await delay(500)
|
|
}
|
|
for (const selector of screen.actions ?? []) {
|
|
await call('Runtime.evaluate', { expression: `document.querySelector(${JSON.stringify(selector)})?.click()` })
|
|
await delay(350)
|
|
}
|
|
const result = await call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
|
|
await writeFile(join(outputDirectory, screen.file), Buffer.from(result.data, 'base64'))
|
|
process.stdout.write(`${screen.file}\n`)
|
|
}
|
|
socket.close()
|
|
} finally {
|
|
chrome.kill()
|
|
}
|
|
|
|
async function waitForTarget() {
|
|
const deadline = Date.now() + 15000
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const targets = await fetch(`http://127.0.0.1:${debuggingPort}/json/list`).then(response => response.json())
|
|
const page = targets.find(target => target.type === 'page')
|
|
if (page) return page
|
|
} catch { /* Chrome is still starting. */ }
|
|
await delay(200)
|
|
}
|
|
throw new Error('Chrome DevTools target did not become available.')
|
|
}
|
|
|
|
function delay(milliseconds) { return new Promise(resolveDelay => setTimeout(resolveDelay, milliseconds)) }
|