diff --git a/.github/actions/nextjs-build-export/action.yml b/.github/actions/nextjs-build-export/action.yml new file mode 100644 index 00000000..37232c15 --- /dev/null +++ b/.github/actions/nextjs-build-export/action.yml @@ -0,0 +1,32 @@ +# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action?platform=mac#creating-an-action-metadata-file +name: nextjs-build-export + +inputs: + e2e: + required: false + default: 'false' + +runs: + using: composite + + steps: + - name: Restore Next.js related caches + uses: actions/cache@v4 + with: + path: | + ${{ github.workspace }}/.next + ${{ github.workspace }}/out + key: ${{ runner.os }}-nextjs-store-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}-${{ inputs.e2e == 'true' && 'e2e' || 'default' }} + restore-keys: | + ${{ runner.os }}-nextjs-store-${{ hashFiles('**/pnpm-lock.yaml') }}- + id: cache-nextjs-build + + - name: Build and Export [default] + shell: bash + if: steps.cache-nextjs-build.outputs.cache-hit != 'true' && inputs.e2e == 'false' + run: pnpm deploy-blog + + - name: Build and Export [e2e] + shell: bash + if: steps.cache-nextjs-build.outputs.cache-hit != 'true' && inputs.e2e == 'true' + run: pnpm e2e:build diff --git a/.github/actions/playwright-install/action.yml b/.github/actions/playwright-install/action.yml new file mode 100644 index 00000000..2f438597 --- /dev/null +++ b/.github/actions/playwright-install/action.yml @@ -0,0 +1,25 @@ +name: playwright-install + +runs: + using: composite + + steps: + # https://github.com/microsoft/playwright/issues/7249#issuecomment-1373375487 + - name: Get playwright version + shell: bash + run: | + echo "PLAYWRIGHT_VERSION=$(node -e "process.stdout.write(require('@playwright/test/package.json').version)")" >> $GITHUB_OUTPUT + id: playwright-version + + - name: Cache Playwright Browsers for Playwright's Version + uses: actions/cache@v4 + with: + # https://playwright.dev/docs/browsers#managing-browser-binaries + path: ~/Library/Caches/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }} + id: cache-playwright-browsers + + - name: Setup Playwright + shell: bash + if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' + run: pnpm e2e:install diff --git a/.github/actions/pnpm-install/action.yml b/.github/actions/pnpm-install/action.yml new file mode 100644 index 00000000..bc348a8d --- /dev/null +++ b/.github/actions/pnpm-install/action.yml @@ -0,0 +1,33 @@ +name: pnpm-install + +runs: + using: composite + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + shell: bash + run: pnpm install diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml new file mode 100644 index 00000000..e42613f9 --- /dev/null +++ b/.github/workflows/code.yml @@ -0,0 +1,30 @@ +name: code + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: ๐ŸŒฑ Install pnpm + uses: ./.github/actions/pnpm-install + + - name: ๐Ÿ Lint + run: pnpm lint + + build-export: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: ๐ŸŒฑ Install pnpm + uses: ./.github/actions/pnpm-install + + - name: ๐Ÿ— Build and Export + uses: ./.github/actions/nextjs-build-export diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml new file mode 100644 index 00000000..b5bfdaae --- /dev/null +++ b/.github/workflows/e2e-reusable.yml @@ -0,0 +1,55 @@ +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +name: e2e-reusable + +on: + workflow_call: + inputs: + others: + type: boolean + default: false + dom-snapshot: + type: boolean + default: false + screen-snapshot: + type: boolean + default: false + +jobs: + i: + timeout-minutes: 60 + runs-on: macos-latest + env: + TZ: Asia/Seoul + + steps: + - uses: actions/checkout@v4 + + - name: ๐ŸŒฑ Install pnpm + uses: ./.github/actions/pnpm-install + + - name: ๐Ÿฅฆ Install playwright + uses: ./.github/actions/playwright-install + + - name: ๐Ÿ— Build and Export + uses: ./.github/actions/nextjs-build-export + with: + e2e: 'true' + + - name: ๐Ÿ„ Run Playwright [others] + if: inputs.others + run: pnpm e2e:others + + - name: ๐ŸงŠ Run Playwright [dom-snapshot] + if: inputs.dom-snapshot + run: pnpm e2e:dom + + - name: ๐Ÿฉธ Run Playwright [screen-snapshot] + if: inputs.screen-snapshot + run: pnpm e2e:screen + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report-$${{ inputs.others && 'others' || inputs.dom-snapshot && 'dom-snapshot' || inputs.screen-snapshot && 'screen-snapshot' || 'no-input-name' }} + path: playwright-report/ + retention-days: 10 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..10b045df --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,23 @@ +name: e2e + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + others: + uses: './.github/workflows/e2e-reusable.yml' + with: + others: true + + dom-snapshot: + uses: './.github/workflows/e2e-reusable.yml' + with: + dom-snapshot: true + + screen-snapshot: + uses: './.github/workflows/e2e-reusable.yml' + with: + screen-snapshot: true diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 6e77665b..00000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Playwright - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - e2e: - timeout-minutes: 60 - runs-on: macos-latest - env: - TZ: Asia/Seoul - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20.x - - - name: Install dependencies - run: npm install -g pnpm && pnpm install - - - name: Install Playwright Browsers - run: pnpm e2e:install - - - name: Build post - run: pnpm e2e:build - - - name: Run Playwright tests - run: pnpm e2e - - - uses: actions/upload-artifact@v4 - if: failure() - with: - name: playwright-report - path: playwright-report/ - retention-days: 10 diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml deleted file mode 100644 index f798a2f2..00000000 --- a/.github/workflows/pr-test.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Test - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - uses: './.github/workflows/resuable.yml' - with: - lint: true - - build-export: - uses: './.github/workflows/resuable.yml' - with: - build-export: true diff --git a/.github/workflows/resuable.yml b/.github/workflows/resuable.yml deleted file mode 100644 index d5d99ad1..00000000 --- a/.github/workflows/resuable.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: reusable - -on: - workflow_call: - inputs: - lint: - type: boolean - default: false - build-export: - type: boolean - default: false - -jobs: - r: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install - - - name: Lint - if: inputs.lint - run: pnpm lint - - - name: Build-export - if: inputs.build-export - run: pnpm deploy-blog diff --git a/e2e/404.spec.ts b/e2e/404.spec.ts index d5286bd9..2a7c3ff9 100644 --- a/e2e/404.spec.ts +++ b/e2e/404.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; -import { screenshotFullPage } from './utils'; +import { screenshotFullPage, waitImages } from './shared/utils'; +import { MACRO_SUITE } from './shared/constants'; test.describe('404', () => { test('should redirect 404', async ({ page }) => { @@ -7,13 +8,13 @@ test.describe('404', () => { await expect(page.getByText(/404 ERROR/)).toBeVisible(); }); - test(`screen`, async ({ page }) => { + test(MACRO_SUITE.SCREEN_SNAPSHOT, async ({ page }) => { await screenshotFullPage({ page, url: `/404`, arg: [`404.png`] }); }); - test(`dom`, async ({ page }) => { + test(MACRO_SUITE.DOM_SNAPSHOT, async ({ page }) => { await page.goto(`/404`); - await page.waitForTimeout(2000); + await waitImages({ page }); const body = await page.locator('#__next').innerHTML(); expect(body).toMatchSnapshot([`404.html`]); }); diff --git a/e2e/__snapshots__/404.spec.ts/desktop/404.html b/e2e/__snapshots__/404.spec.ts/desktop/404.html index e2a569f2..cba0340e 100644 --- a/e2e/__snapshots__/404.spec.ts/desktop/404.html +++ b/e2e/__snapshots__/404.spec.ts/desktop/404.html @@ -1,4 +1,4 @@ -
surfing

+
surfing

 
       404 ERROR
 
diff --git a/e2e/__snapshots__/404.spec.ts/mobile/404.html b/e2e/__snapshots__/404.spec.ts/mobile/404.html
index e2a569f2..cba0340e 100644
--- a/e2e/__snapshots__/404.spec.ts/mobile/404.html
+++ b/e2e/__snapshots__/404.spec.ts/mobile/404.html
@@ -1,4 +1,4 @@
-
surfing

+
surfing

 
       404 ERROR
 
diff --git a/e2e/about.spec.ts b/e2e/about.spec.ts
index 7a068db9..83dcfc80 100644
--- a/e2e/about.spec.ts
+++ b/e2e/about.spec.ts
@@ -1,12 +1,13 @@
 import { expect, test } from '@playwright/test';
-import { screenshotFullPage } from './utils';
+import { screenshotFullPage } from './shared/utils';
+import { MACRO_SUITE } from './shared/constants';
 
 test.describe('about', () => {
-  test(`screen`, async ({ page }) => {
+  test(MACRO_SUITE.SCREEN_SNAPSHOT, async ({ page }) => {
     await screenshotFullPage({ page, url: `/about`, arg: [`about.png`] });
   });
 
-  test(`dom`, async ({ page }) => {
+  test(MACRO_SUITE.DOM_SNAPSHOT, async ({ page }) => {
     await page.goto(`/about`);
     const body = await page.locator('#__next').innerHTML();
     expect(body).toMatchSnapshot([`about.html`]);
diff --git a/e2e/post/dom.spec.ts b/e2e/post/dom.spec.ts
index 03c31de0..80a352a1 100644
--- a/e2e/post/dom.spec.ts
+++ b/e2e/post/dom.spec.ts
@@ -1,14 +1,14 @@
 import { expect, test } from '@playwright/test';
 import { urls } from './utils';
+import { MACRO_SUITE } from 'e2e/shared/constants';
 
-test.describe('dom', () => {
+test.describe(MACRO_SUITE.DOM_SNAPSHOT, () => {
   for (let i = 0; i < urls.length; i++) {
     const url = urls[i];
 
     test(`${url}`, async ({ page }) => {
       await page.goto(`/posts/${url}`);
-      // TODO: floating์ด ๊นœ๋นก์ด๋Š” ๋ฌธ์ œ๊ฐ€ ์กด์žฌํ•จ.
-      await page.waitForTimeout(1000);
+      await page.evaluate(() => document.fonts.ready);
       const body = await page.locator('#__next').innerHTML();
       expect(body).toMatchSnapshot([`${url}.html`]);
     });
diff --git a/e2e/post/screen.spec.ts b/e2e/post/screen.spec.ts
index 8dc55749..203a8e3f 100644
--- a/e2e/post/screen.spec.ts
+++ b/e2e/post/screen.spec.ts
@@ -1,8 +1,9 @@
 import { test } from '@playwright/test';
 import { urls } from './utils';
-import { screenshotFullPage } from 'e2e/utils';
+import { screenshotFullPage } from 'e2e/shared/utils';
+import { MACRO_SUITE } from 'e2e/shared/constants';
 
-test.describe(`screen`, () => {
+test.describe(MACRO_SUITE.SCREEN_SNAPSHOT, () => {
   for (let i = 0; i < urls.length; i++) {
     const url = urls[i];
 
diff --git a/e2e/posts.spec.ts b/e2e/posts.spec.ts
index af7c91f9..ffb12fa5 100644
--- a/e2e/posts.spec.ts
+++ b/e2e/posts.spec.ts
@@ -1,9 +1,10 @@
 import { expect, test } from '@playwright/test';
+import { MACRO_SUITE } from './shared/constants';
 
 test.describe('posts', () => {
-  test(`dom`, async ({ page }) => {
+  test(MACRO_SUITE.DOM_SNAPSHOT, async ({ page }) => {
     await page.goto(`/posts`);
-    await page.waitForTimeout(1000);
+    await page.evaluate(() => document.fonts.ready);
     const body = await page.locator('#__next').innerHTML();
     expect(body).toMatchSnapshot([`posts.html`]);
   });
diff --git a/e2e/shared/constants.ts b/e2e/shared/constants.ts
new file mode 100644
index 00000000..64d63fd2
--- /dev/null
+++ b/e2e/shared/constants.ts
@@ -0,0 +1,5 @@
+// https://stackoverflow.com/a/76497222
+export enum MACRO_SUITE {
+  DOM_SNAPSHOT = '@dom-snapshot',
+  SCREEN_SNAPSHOT = '@screen-snapshot',
+}
diff --git a/e2e/utils.ts b/e2e/shared/utils.ts
similarity index 50%
rename from e2e/utils.ts
rename to e2e/shared/utils.ts
index e5834066..0a7fc454 100644
--- a/e2e/utils.ts
+++ b/e2e/shared/utils.ts
@@ -6,6 +6,20 @@ type ScreenshotOptions = {
   clip?: { x: number; y: number; width: number; height: number };
 };
 
+export const waitImages = async ({ page }: { page: Page }) => {
+  // https://stackoverflow.com/questions/77287441/how-to-wait-for-full-rendered-image-in-playwright
+  const locators = page.locator('//img');
+  await locators.evaluateAll((e) => e.map((i) => i.scrollIntoView()));
+  // Set up listeners concurrently
+  const promises = (await locators.all()).map((locator) =>
+    locator.evaluate(
+      (image) => image.complete || new Promise((f) => (image.onload = f)),
+    ),
+  );
+  // Wait for all once
+  await Promise.all(promises);
+};
+
 export const screenshotFullPage = async ({
   page,
   url,
@@ -19,18 +33,11 @@ export const screenshotFullPage = async ({
 }) => {
   await page.goto(url);
   await page.evaluate(() => document.fonts.ready);
-  // https://github.com/microsoft/playwright/issues/18827#issuecomment-2031261562
-  await page.locator('#__next').scrollIntoViewIfNeeded();
-  await page.waitForTimeout(1000);
 
-  // const { width } = page.viewportSize();
-  // const height = await page.evaluate(
-  //   () => document.scrollingElement.scrollHeight,
-  // );
+  await waitImages({ page });
 
   const options: ScreenshotOptions = {
     fullPage: true,
-    // clip: { x: 0, y: 0, width, height },
   };
   if (timeout >= 0) {
     options.timeout = timeout;
diff --git a/package.json b/package.json
index 237a1511..074803c7 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,9 @@
     "e2e:update": "pnpm playwright test --update-snapshots",
     "e2e:report": "pnpm exec playwright show-report",
     "e2e:install": "pnpm exec playwright install --with-deps",
+    "e2e:others": "pnpm playwright test --grep-invert /@/",
+    "e2e:dom": "pnpm playwright test --grep '@dom-snapshot'",
+    "e2e:screen": "pnpm playwright test --grep '@screen-snapshot'",
     "e2e": "pnpm playwright test",
     "clean": "rm -rf .next out node_modules",
     "knip": "knip",
diff --git a/src/features/404/Container.tsx b/src/features/404/Container.tsx
index b2baf15b..8c9d9e3e 100644
--- a/src/features/404/Container.tsx
+++ b/src/features/404/Container.tsx
@@ -19,6 +19,7 @@ const NotFoundContainer: FunctionComponent = () => {