diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..f840703 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,80 @@ +name: E2E Tests + +on: + pull_request: + paths: + - "projects/project-generator/**" + push: + branches: + - main + paths: + - "projects/project-generator/**" + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.13.1 + + - name: Install dependencies + run: pnpm install + + - name: Build project generator + run: pnpm --filter create-vite-apollo-fs build + + - name: Generate test project + run: | + chmod +x ./projects/project-generator/dist/generator.js + ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui + + - name: Install dependencies in generated project + run: | + cd myProject + pnpm install + + - name: Start development server in background + run: | + cd myProject + pnpm dev & + echo $! > dev_server.pid + + - name: Wait for server to be ready + run: | + timeout 60 bash -c 'until curl -f http://localhost:5173; do sleep 2; done' + + - name: Install Playwright browsers and dependencies + run: | + cd projects/project-generator-e2e + npx playwright install --with-deps + + - name: Run E2E tests + run: | + cd projects/project-generator-e2e + npm run test + + - name: Stop development server + if: always() + run: | + if [ -f myProject/dev_server.pid ]; then + kill $(cat myProject/dev_server.pid) || true + fi + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: projects/project-generator-e2e/playwright-report/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c763054..0a53c19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,15 @@ importers: specifier: ~5.8.3 version: 5.8.3 + projects/project-generator-e2e: + devDependencies: + '@playwright/test': + specifier: ^1.54.1 + version: 1.54.1 + '@types/node': + specifier: ^24.1.0 + version: 24.1.0 + projects/web-ui: dependencies: '@apollo/client': @@ -960,6 +969,11 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@playwright/test@1.54.1': + resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} + engines: {node: '>=18'} + hasBin: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1951,6 +1965,11 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2628,6 +2647,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4213,6 +4242,10 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@playwright/test@1.54.1': + dependencies: + playwright: 1.54.1 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5257,6 +5290,9 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5880,6 +5916,14 @@ snapshots: picomatch@4.0.3: {} + playwright-core@1.54.1: {} + + playwright@1.54.1: + dependencies: + playwright-core: 1.54.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.5.6: diff --git a/projects/api/src/utils/logger.ts b/projects/api/src/utils/logger.ts new file mode 100644 index 0000000..e69de29 diff --git a/projects/api/tsconfig.json b/projects/api/tsconfig.json index f54c2c4..4bd690d 100644 --- a/projects/api/tsconfig.json +++ b/projects/api/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig-base.json", + "extends": "../tsconfig.base.json", "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], diff --git a/projects/project-generator-e2e/.gitignore b/projects/project-generator-e2e/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/projects/project-generator-e2e/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/projects/project-generator-e2e/package.json b/projects/project-generator-e2e/package.json new file mode 100644 index 0000000..e01c36d --- /dev/null +++ b/projects/project-generator-e2e/package.json @@ -0,0 +1,13 @@ +{ + "name": "project-generator-e2e", + "version": "1.0.0", + "scripts": { + "test": "playwright test", + "show-report": "playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.54.1", + "@types/node": "^24.1.0" + }, + "packageManager": "pnpm@10.13.1" +} diff --git a/projects/project-generator-e2e/playwright.config.ts b/projects/project-generator-e2e/playwright.config.ts new file mode 100644 index 0000000..390646d --- /dev/null +++ b/projects/project-generator-e2e/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/projects/project-generator-e2e/tests/can-add-user.test.ts b/projects/project-generator-e2e/tests/can-add-user.test.ts new file mode 100644 index 0000000..7c37b82 --- /dev/null +++ b/projects/project-generator-e2e/tests/can-add-user.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("can add user", async ({ page }) => { + await page.goto(url); + + // Check if the add user input and button are visible + const addUserInput = page.locator('input[data-testid="add-user-input"]'); + const addUserButton = page.locator('button[data-testid="add-user-button"]'); + + await expect(addUserInput).toBeVisible(); + await expect(addUserButton).toBeVisible(); + + // Add a new user + const newUserName = "TestUser"; + await addUserInput.fill(newUserName); + await addUserButton.click(); + + await page.waitForTimeout(3000); + + // Verify the new user is added to the list + const usersList = page.locator('ul[data-testid="users-list"]'); + const newUserLocator = usersList + .locator(`li:has-text("${newUserName}")`) + .first(); + + await expect(newUserLocator).toBeVisible(); +}); diff --git a/projects/project-generator-e2e/tests/has-greetings.test.ts b/projects/project-generator-e2e/tests/has-greetings.test.ts new file mode 100644 index 0000000..b8b413f --- /dev/null +++ b/projects/project-generator-e2e/tests/has-greetings.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("hello query has one of known greetings", async ({ page }) => { + const knownGreetings = ["Hello!", "Hi there!", "Welcome!", "Good day!"]; + await page.goto(url); + const helloQuery = page.locator('span[data-testid="greetings-subscription"]'); + await expect(helloQuery).toBeVisible(); + await page.waitForTimeout(3000); + const helloText = await helloQuery.textContent(); + expect(helloText).toBeDefined(); + expect(knownGreetings).toContain(helloText); +}); diff --git a/projects/project-generator-e2e/tests/has-title.test.ts b/projects/project-generator-e2e/tests/has-title.test.ts new file mode 100644 index 0000000..a6acd1d --- /dev/null +++ b/projects/project-generator-e2e/tests/has-title.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("has title", async ({ page }) => { + await page.goto(url); + await expect(page).toHaveTitle("Vite + React + TS"); +}); diff --git a/projects/project-generator-e2e/tests/has-users.test.ts b/projects/project-generator-e2e/tests/has-users.test.ts new file mode 100644 index 0000000..295a790 --- /dev/null +++ b/projects/project-generator-e2e/tests/has-users.test.ts @@ -0,0 +1,19 @@ +import test, { expect } from "@playwright/test"; +import { url } from "./url"; + +test("has users", async ({ page }) => { + await page.goto(url); + const usersList = page.locator('ul[data-testid="users-list"]'); + await expect(usersList).toBeVisible(); + + // Check if the list has at least one user + const usersCount = await usersList.locator("li").count(); + expect(usersCount).toBeGreaterThan(0); + + // Optionally, check for specific users + const knownUsers = ["John Doe", "Jane Smith"]; + for (const user of knownUsers) { + const userLocator = usersList.locator(`li:has-text("${user}")`); + await expect(userLocator).toBeVisible(); + } +}); diff --git a/projects/project-generator-e2e/tests/url.ts b/projects/project-generator-e2e/tests/url.ts new file mode 100644 index 0000000..417d55f --- /dev/null +++ b/projects/project-generator-e2e/tests/url.ts @@ -0,0 +1 @@ +export const url = "http://localhost:5173/movies-with-actors"; diff --git a/projects/project-generator-e2e/tsconfig.json b/projects/project-generator-e2e/tsconfig.json new file mode 100644 index 0000000..4cc1db9 --- /dev/null +++ b/projects/project-generator-e2e/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "@playwright/test"] + }, + "include": ["tests/**/*", "playwright.config.ts"], + "exclude": ["node_modules"] +} diff --git a/projects/project-generator/src/generator.ts b/projects/project-generator/src/generator.ts index 28962ed..9b78f76 100644 --- a/projects/project-generator/src/generator.ts +++ b/projects/project-generator/src/generator.ts @@ -234,7 +234,7 @@ async function generateWebUiProject(config: ProjectConfig) { // Add graphqlLoader() to plugins array if not present viteConfig = viteConfig.replace( /(plugins:\s*\[)([^\]]*)\]/, - (match: string, p1: string, p2: string) => { + (_: string, p1: string, p2: string) => { let plugins = p2.trim().replace(/,$/, ""); if (!plugins.includes("graphqlLoader()")) plugins += ", graphqlLoader()"; diff --git a/projects/project-generator/templates/web-ui/Dashboard.tsx b/projects/project-generator/templates/web-ui/Dashboard.tsx index 7159a1c..4d11b35 100644 --- a/projects/project-generator/templates/web-ui/Dashboard.tsx +++ b/projects/project-generator/templates/web-ui/Dashboard.tsx @@ -61,7 +61,8 @@ function Dashboard() {

Loading...

) : (

- Hello Query: {helloData?.hello} + Hello Query:{" "} + {helloData?.hello}

)} @@ -79,7 +80,10 @@ function Dashboard() { {usersLoading ? (

Loading users...

) : ( -