diff --git a/testing/report/report.config.js b/testing/report/report.config.js index d66aa52b..2eba836d 100644 --- a/testing/report/report.config.js +++ b/testing/report/report.config.js @@ -1,13 +1,17 @@ -const env = require("dotenv").config(); +const path = require("path"); -const { OUTDIR, LOCAL_ARCHIVE_FILE, LOCAL_ARCHIVE_IMAGE } = env; +require("dotenv").config({ + path: path.join(__dirname, ".env"), +}); + +const { OUTDIR, LOCAL_ARCHIVE_FILES, LOCAL_ARCHIVE_IMAGES } = process.env; module.exports = { - sample: "../../data/figma-archives/dev/meta.json", + sample: path.join(__dirname, "../../data/figma-archives/prod/meta.json"), outDir: OUTDIR, localarchive: { - file: LOCAL_ARCHIVE_FILE, - image: LOCAL_ARCHIVE_IMAGE, + files: LOCAL_ARCHIVE_FILES, + images: LOCAL_ARCHIVE_IMAGES, }, skipIfReportExists: true, }; diff --git a/testing/report/src/index.ts b/testing/report/src/index.ts index 5cd118c8..0a27e522 100644 --- a/testing/report/src/index.ts +++ b/testing/report/src/index.ts @@ -1,6 +1,5 @@ import path from "path"; import fs from "fs/promises"; -import { existsSync as exists } from "fs"; import assert from "assert"; import ora from "ora"; import { mapper } from "@design-sdk/figma-remote"; @@ -20,51 +19,75 @@ import { RemoteImageRepositories } from "@design-sdk/figma-remote/asset-reposito setupCache(axios); -const mkdir = (path: string) => !exists(path) && fs.mkdir(path); +const exists = async (path: string) => { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +}; + +const mkdir = async (path: string) => + !(await exists(path)) && (await fs.mkdir(path)); interface ReportConfig { sample: string; outDir?: string; localarchive?: { - file: string; - image: string; + files: string; + images: string; }; skipIfReportExists?: boolean; } // disable logging console.log = () => {}; -console.info = () => {}; console.warn = () => {}; console.error = () => {}; async function report() { + console.info("Starting report"); const cwd = process.cwd(); // read the config const config: ReportConfig = require(path.join(cwd, "report.config.js")); // load the sample file - const samples_path = path.join(cwd, config.sample); + const samples_path = (await exists(config.sample)) + ? config.sample + : path.join(cwd, config.sample); + + assert( + await exists(samples_path), + `sample file not found at ${config.sample} nor ${samples_path}` + ); + const samples = JSON.parse(await fs.readFile(samples_path, "utf-8")); // create .coverage folder const coverage_path = config.outDir ?? path.join(cwd, ".coverage"); - mkdir(coverage_path); + + console.info(`Loaded ${samples.length} samples`); + console.info(`Configuration used - ${JSON.stringify(config, null, 2)}`); + + await mkdir(coverage_path); const client = Client({ paths: { - file: config.localarchive.file, - image: config.localarchive.image, + files: config.localarchive.files, + images: config.localarchive.images, }, }); const ssworker = new ScreenshotWorker({}); await ssworker.launch(); + let i = 0; for (const c of samples) { + i++; // create .coverage/:id folder const coverage_set_path = path.join(coverage_path, c.id); - mkdir(coverage_set_path); + await mkdir(coverage_set_path); const { id: filekey } = c; let file; @@ -93,21 +116,26 @@ async function report() { }) ).data.images; } catch (e) { - console.error("exports not ready for", filekey); + console.error("exports not ready for", filekey, e.message); continue; } + let ii = 0; for (const frame of frames) { - const spinner = ora(`Running coverage for ${c.id}/${frame.id}`).start(); + ii++; + + const spinner = ora( + `[${i}/${samples.length}] Running coverage for ${c.id}/${frame.id} (${ii}/${frames.length})` + ).start(); // create .coverage/:id/:node folder const coverage_node_path = path.join(coverage_set_path, frame.id); - mkdir(coverage_node_path); + await mkdir(coverage_node_path); // report.json const report_file = path.join(coverage_node_path, "report.json"); if (config.skipIfReportExists) { - if (exists(report_file)) { + if (await exists(report_file)) { spinner.succeed(`Skipping - report for ${frame.id} already exists`); continue; } @@ -134,6 +162,37 @@ async function report() { ); try { + // image A (original) + const exported = exports[frame.id]; + const image_a_rel = "./a.png"; + const image_a = path.join(coverage_node_path, image_a_rel); + // download the exported image with url + // if the exported is local fs path, then use copy instead + if (await exists(exported)) { + try { + // copy file with symlink + // rempve if already exists before linking new one + if (await exists(image_a)) { + await fs.unlink(image_a); + } + await fs.symlink(exported, image_a); + } catch (e) { + // TODO: symlink still fails with "EEXIST: file already exists, symlink" + // we need to handle this. + // reason? - unknown + } + } else if (exported.startsWith("http")) { + const dl = await axios.get(exported, { responseType: "arraybuffer" }); + await fs.writeFile(image_a, dl.data); + } else { + throw new Error(`File not found - ${exported}`); + } + + if (!(await exists(image_a))) { + spinner.fail(`Image A not found - ${image_a}`); + continue; + } + // codegen const code = await htmlcss( { @@ -165,23 +224,6 @@ async function report() { const image_b = path.join(coverage_node_path, image_b_rel); await fs.writeFile(image_b, screenshot_buffer); - const exported = exports[frame.id]; - const image_a_rel = "./a.png"; - const image_a = path.join(coverage_node_path, image_a_rel); - // download the exported image with url - // if the exported is local fs path, then use copy instead - if (exists(exported)) { - // copy file with symlink - // unlink if exists - if (exists(image_a)) { - await fs.unlink(image_a); - } - await fs.symlink(exported, image_a); - } else { - const dl = await axios.get(exported, { responseType: "arraybuffer" }); - await fs.writeFile(image_a, dl.data); - } - const diff = await resemble(image_a, image_b); const diff_file = path.join(coverage_node_path, "diff.png"); // write diff.png @@ -224,6 +266,13 @@ async function report() { spinner.fail(`error on ${frame.id} : ${e.message}`); } } + + // cleaup + // if the coverage is empty, remove the folder + const files = await fs.readdir(coverage_set_path); + if (files.length === 0) { + await fs.rmdir(coverage_set_path); + } } // cleaup diff --git a/testing/testing-screenshot/index.ts b/testing/testing-screenshot/index.ts index c8729a96..a3ddc7d2 100644 --- a/testing/testing-screenshot/index.ts +++ b/testing/testing-screenshot/index.ts @@ -8,14 +8,6 @@ interface ScreenshotOptions { }; } -export async function screenshot({ htmlcss, viewport }: ScreenshotOptions) { - const worker = new Worker({}); - await worker.launch(); - const buffer = worker.screenshot({ htmlcss, viewport }); - await worker.terminate(); - return buffer; -} - export class Worker { private browser: Browser; private page: Page; @@ -40,22 +32,53 @@ export class Worker { return this.browser; } + async relaunch() { + await this.close(); + return this.launch(); + } + async screenshot({ htmlcss, viewport }: ScreenshotOptions) { - this.page.setViewport(viewport); - await this.page.setContent(htmlcss, { waitUntil: "networkidle0" }); - const buffer = await this.page.screenshot({ - type: "png", - // support transparency - omitBackground: true, - }); - return buffer; + try { + if (!this.browser || !this.page || this.page.isClosed()) { + await this.relaunch(); + } + await this.page.setViewport(viewport); + await this.page.setContent(htmlcss, { waitUntil: "networkidle0" }); + const buffer = await this.page.screenshot({ + type: "png", + // support transparency + omitBackground: true, + }); + return buffer; + } catch (error) { + console.log(`Failed to take screenshot: ${error.message}`); + await this.relaunch(); + // After relaunch, retry taking screenshot or rethrow the error + return this.screenshot({ htmlcss, viewport }); + } } async close() { - await this.browser.close(); + if (this.browser) { + try { + await this.browser.close(); + } catch (e) { + console.log(`Failed to close browser: ${e.message}`); + } + this.browser = null; + this.page = null; + } } terminate() { this.close(); } } + +export async function screenshot(options: ScreenshotOptions) { + const worker = new Worker({}); + await worker.launch(); + const buffer = await worker.screenshot(options); + await worker.terminate(); + return buffer; +}