ocre-cli 0.1.1

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! `ocre generate system_test <name>`: a browser test (Rails' system tests) in
//! `tests/system/<name>.spec.ts`, run by Playwright against the test server
//! of `ocre test --e2e`. The first one also writes `playwright.config.ts`
//! (desktop and phone screens, a screenshot of each failure) and adds
//! `@playwright/test` to package.json.

use super::Edits;
use crate::{
    CliResult,
    names::{humanize, is_identifier},
    output::CliError,
    project::Project,
};

/// Directory of the system tests, relative to the app root.
pub const SYSTEM_TESTS: &str = "tests/system";

/// Pinned `@playwright/test` release.
pub const PLAYWRIGHT_VERSION: &str = "1.63.0";

const CONFIG: &str = r#"// System tests (`ocre g system_test`): a real browser against the test server
// that `ocre test --e2e` starts, which sets BASE_URL. `npx playwright test --ui`
// runs them interactively against `ocre dev` (BASE_URL=http://localhost:8787).
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "tests/system",
  // The test server shares one database: one test at a time.
  workers: 1,
  use: {
    baseURL: process.env.BASE_URL ?? "http://localhost:8788",
    // Failed tests keep a screenshot and a trace in test-results/.
    screenshot: "only-on-failure",
    trace: "retain-on-failure",
  },
  // Each test runs once per screen. Other browsers: devices["Desktop Firefox"],
  // devices["Desktop Safari"] (then `npx playwright install firefox webkit`).
  projects: [
    { name: "desktop", use: { ...devices["Desktop Chrome"] } },
    { name: "phone", use: { ...devices["Pixel 7"] } },
  ],
});
"#;

pub fn system_test(project: &Project, name: &str) -> CliResult {
    if !is_identifier(name) {
        return Err(CliError::new(format!("invalid system test name `{name}`"))
            .hint("use snake_case starting with a letter, e.g. `ocre g system_test signing_up`"));
    }
    let mut edits = Edits::new(project);
    if !edits.exists("playwright.config.ts") {
        edits.create("playwright.config.ts", CONFIG.to_owned())?;
        let package = edits.read("package.json")?.unwrap_or_default();
        let marker = "\"devDependencies\": {\n";
        let at = package.find(marker).ok_or_else(|| {
            CliError::new("package.json has no `\"devDependencies\": {` block")
                .hint("restore the package.json `ocre new` writes, or add `\"@playwright/test\"` to it by hand")
        })?;
        let line = format!("    \"@playwright/test\": \"{PLAYWRIGHT_VERSION}\",\n");
        let at = at + marker.len();
        edits.update("package.json", format!("{}{line}{}", &package[..at], &package[at..]));
        let ignore = edits.read(".gitignore")?.unwrap_or_default();
        if !ignore.contains("test-results/") {
            edits.update(
                ".gitignore",
                format!("{ignore}# Playwright (ocre g system_test)\ntest-results/\nplaywright-report/\n"),
            );
        }
    }
    let title = humanize(name);
    edits.create(
        &format!("{SYSTEM_TESTS}/{name}.spec.ts"),
        format!(
            r#"// {title}: a browser test. Generated by `ocre g system_test {name}`; runs with `ocre test --e2e`.
// Assertions wait for the page (https://playwright.dev/docs/test-assertions).
import {{ expect, test }} from "@playwright/test";

test("{title}", async ({{ page }}) => {{
  await page.goto("/");
  await expect(page.locator("body")).toBeVisible();
  // await page.getByRole("link", {{ name: "New post" }}).click();
  // await page.getByLabel("Title").fill("Hello");
  // await page.getByRole("button", {{ name: "Save" }}).click();
  // await expect(page.getByText("Post was successfully created.")).toBeVisible();
}});
"#
        ),
    )?;
    let mut report = edits.apply("generate system_test")?;
    report.next = vec![
        "npm install".to_owned(),
        "npx playwright install chromium (once per machine)".to_owned(),
        format!("edit {SYSTEM_TESTS}/{name}.spec.ts, then ocre test --e2e"),
    ];
    Ok(report)
}