Back to Repositories

Testing Screenshot-to-Code Generation and Updates in abi/screenshot-to-code

This test suite implements end-to-end testing for the screenshot-to-code application, focusing on code generation capabilities across different models and technology stacks. It validates image upload, URL-based generation, and code editing functionalities using Puppeteer for browser automation.

Test Coverage Overview

The test suite provides comprehensive coverage of core application features:
  • Screenshot-to-code generation across multiple AI models
  • URL-based code generation
  • Code updates and version management
  • Multi-stack support (HTML/Tailwind)
  • Edge cases handling for different input types

Implementation Analysis

The testing approach utilizes a class-based structure with Puppeteer for browser automation. It implements page object pattern through the App class, encapsulating test setup and interactions.

The implementation leverages TypeScript’s type safety with interfaces for Browser and Page objects, while utilizing Jest’s describe/it blocks for test organization.

Technical Details

  • Testing Framework: Jest
  • Browser Automation: Puppeteer
  • Language: TypeScript
  • Environment Setup: Local storage configuration, viewport settings
  • Test Timeouts: Configured for long-running operations
  • Screenshot Capture: Automated visual verification

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Modular test organization with reusable setup
  • Proper test isolation and cleanup
  • Async/await pattern usage
  • Explicit waiting strategies
  • Environment-specific configuration
  • Comprehensive error handling

abi/screenshot-to-code

frontend/src/tests/qa.test.ts

            
import puppeteer, { Browser, Page, ElementHandle } from "puppeteer";
import { Stack } from "../lib/stacks";
import { CodeGenerationModel } from "../lib/models";

const TESTS_ROOT_PATH = process.env.TEST_ROOT_PATH;

// Fixtures
const FIXTURES_PATH = `${TESTS_ROOT_PATH}/fixtures`;
const SIMPLE_SCREENSHOT = FIXTURES_PATH + "/simple_button.png";
const SCREENSHOT_WITH_IMAGES = `${FIXTURES_PATH}/simple_ui_with_image.png`;

// Results
const RESULTS_DIR = `${TESTS_ROOT_PATH}/results`;

describe("e2e tests", () => {
  let browser: Browser;
  let page: Page;

  const DEBUG = true;
  const IS_HEADLESS = true;

  const stacks = Object.values(Stack).slice(0, DEBUG ? 1 : undefined);
  const models = DEBUG
    ? [
        CodeGenerationModel.GPT_4O_2024_05_13,
        // CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20,
      ]
    : Object.values(CodeGenerationModel);

  beforeAll(async () => {
    browser = await puppeteer.launch({ headless: IS_HEADLESS });
    page = await browser.newPage();
    await page.goto("http://localhost:5173/");

    // Set screen size
    await page.setViewport({ width: 1080, height: 1024 });

    // TODO: Does this need to be moved?
    // const client = await page.createCDPSession();
    // Set download behavior path
    // await client.send("Page.setDownloadBehavior", {
    //   behavior: "allow",
    //   downloadPath: DOWNLOAD_PATH,
    // });
  });

  afterAll(async () => {
    await browser.close();
  });

  // Create tests
  models.forEach((model) => {
    stacks.forEach((stack) => {
      it(
        `Create for : ${model} & ${stack}`,
        async () => {
          const app = new App(
            page,
            stack,
            model,
            `create_screenshot_${model}_${stack}`
          );
          await app.init();
          // Generate from screenshot
          await app.uploadImage(SCREENSHOT_WITH_IMAGES);
        },
        60 * 1000
      );

      it(
        `Create from URL for : ${model} & ${stack}`,
        async () => {
          const app = new App(
            page,
            stack,
            model,
            `create_url_${model}_${stack}`
          );
          await app.init();
          // Generate from screenshot
          await app.generateFromUrl("https://a.picoapps.xyz/design-fear");
        },
        60 * 1000
      );
    });
  });

  // Update tests - for every model (doesn’t need to be repeated for each stack - fix to HTML Tailwind only)
  models.forEach((model) => {
    ["html_tailwind"].forEach((stack) => {
      it(
        `update: ${model}`,
        async () => {
          const app = new App(page, stack, model, `update_${model}_${stack}`);
          await app.init();

          // Generate from screenshot
          await app.uploadImage(SIMPLE_SCREENSHOT);
          // Regenerate works for v1
          await app.regenerate();
          // Make an update
          await app.edit("make the button background blue", "v2");
          // Make another update
          await app.edit("make the text italic", "v3");
          // Branch off v2 and make an update
          await app.clickVersion("v2");
          await app.edit("make the text yellow", "v4");
        },
        90 * 1000
      );
    });
  });

  // Start from code tests - for every model
  models.forEach((model) => {
    ["html_tailwind"].forEach((stack) => {
      it.skip(
        `Start from code: ${model}`,
        async () => {
          const app = new App(
            page,
            stack,
            model,
            `start_from_code_${model}_${stack}`
          );
          await app.init();

          await app.importFromCode();

          // Regenerate works for v1
          // await app.regenerate();
          // // Make an update
          // await app.edit("make the header blue", "v2");
          // // Make another update
          // await app.edit("make all text italic", "v3");
          // // Branch off v2 and make an update
          // await app.clickVersion("v2");
          // await app.edit("make all text red", "v4");
        },
        90 * 1000
      );
    });
  });
});

class App {
  private screenshotPathPrefix: string;
  private page: Page;
  private stack: string;
  private model: string;

  constructor(page: Page, stack: string, model: string, testId: string) {
    this.page = page;
    this.stack = stack;
    this.model = model;
    this.screenshotPathPrefix = `${RESULTS_DIR}/${testId}`;
  }

  async init() {
    await this.setupLocalStorage();
  }

  async setupLocalStorage() {
    const setting = {
      openAiApiKey: null,
      openAiBaseURL: null,
      screenshotOneApiKey: process.env.TEST_SCREENSHOTONE_API_KEY,
      isImageGenerationEnabled: true,
      editorTheme: "cobalt",
      generatedCodeConfig: this.stack,
      codeGenerationModel: this.model,
      isTermOfServiceAccepted: false,
      accessCode: null,
    };

    await this.page.evaluate((setting) => {
      localStorage.setItem("setting", JSON.stringify(setting));
    }, setting);

    // Reload the page to apply the local storage
    await this.page.reload();
  }

  async _screenshot(step: string) {
    await this.page.screenshot({
      path: `${this.screenshotPathPrefix}_${step}.png`,
    });
  }

  async _waitUntilVersionIsReady(version: string) {
    await this.page.waitForNetworkIdle();
    await this.page.waitForFunction(
      (version) => document.body.innerText.includes(version),
      {
        timeout: 30000,
      },
      version
    );
    // Wait for 3s so that the HTML and JS has time to render before screenshotting
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }

  async generateFromUrl(url: string) {
    // Type in the URL
    await this.page.type('input[placeholder="Enter URL"]', url);
    await this._screenshot("typed_url");

    // Click the capture button and wait for the code to be generated
    await this.page.click("button.capture-btn");
    await this._waitUntilVersionIsReady("v1");
    await this._screenshot("url_result");
  }

  // Uploads a screenshot and generates the image
  async uploadImage(screenshotPath: string) {
    // Upload file
    const fileInput = (await this.page.$(
      ".file-input"
    )) as ElementHandle<HTMLInputElement>;
    if (!fileInput) {
      throw new Error("File input element not found");
    }
    await fileInput.uploadFile(screenshotPath);
    await this._screenshot("image_uploaded");

    // Click the generate button and wait for the code to be generated
    await this._waitUntilVersionIsReady("v1");
    await this._screenshot("image_results");
  }

  // Makes a text edit and waits for a new version
  async edit(edit: string, version: string) {
    // Type in the edit
    await this.page.type(
      'textarea[placeholder="Tell the AI what to change..."]',
      edit
    );
    await this._screenshot(`typed_${version}`);

    // Click the update button and wait for the code to be generated
    await this.page.click(".update-btn");
    await this._waitUntilVersionIsReady(version);
    await this._screenshot(`done_${version}`);
  }

  async clickVersion(version: string) {
    await this.page.evaluate((version) => {
      document.querySelectorAll("div").forEach((div) => {
        if (div.innerText.includes(version)) {
          div.click();
        }
      });
    }, version);
  }

  async regenerate() {
    await this.page.click(".regenerate-btn");
    await this._waitUntilVersionIsReady("v1");
    await this._screenshot("regenerate_results");
  }

  // Work in progress
  async importFromCode() {
    await this.page.click(".import-from-code-btn");

    await this.page.type("textarea", "<html>hello world</html>");

    await this.page.select("#output-settings-js", "HTML + Tailwind");

    await this._screenshot("typed_code");

    await this.page.click(".import-btn");

    await this._waitUntilVersionIsReady("v1");
  }
}