mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
test: puppeteer -> playwright
This commit is contained in:
parent
5a687cb9f5
commit
7560895fff
43 changed files with 548 additions and 1324 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["webpack.config.ts", "*.tsx?"],
|
"files": ["webpack.config.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"excludedFiles": ["*.d.ts"],
|
"excludedFiles": ["*.d.ts"],
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
|
|
||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -22,8 +22,6 @@ jobs:
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
|
||||||
run: |
|
run: |
|
||||||
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
||||||
|
|
||||||
|
|
|
||||||
25
.github/workflows/tests.yml
vendored
25
.github/workflows/tests.yml
vendored
|
|
@ -47,17 +47,32 @@ jobs:
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
|
||||||
run: |
|
run: |
|
||||||
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install chromium
|
||||||
|
|
||||||
- name: E2E Test
|
- name: E2E Test
|
||||||
uses: mymindstorm/puppeteer-headful@8f745c770f7f4c0f9f332d7c43a775f90e53779a
|
|
||||||
env:
|
env:
|
||||||
CI: 'true'
|
CI: 'true'
|
||||||
|
run: xvfb-run yarn test
|
||||||
|
|
||||||
|
- name: Upload Playwright Report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
args: yarn test
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: test-results/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
unit-test:
|
unit-test:
|
||||||
needs: build
|
needs: build
|
||||||
|
|
@ -86,8 +101,6 @@ jobs:
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
|
||||||
run: |
|
run: |
|
||||||
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
|
||||||
|
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ dist-firefox
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
/vscode-icons
|
/vscode-icons
|
||||||
firefox-profile
|
firefox-profile
|
||||||
|
/test-results
|
||||||
|
/playwright-report
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts"],
|
|
||||||
"excludedFiles": ["*.d.ts"],
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
// TS files cannot be transformed without this babel config
|
|
||||||
module.exports = require('../babel.config')
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/**
|
|
||||||
* Confirm basic behaviors of puppeteer assertions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToFind, expectToNotFind, expectToReject } from '../utils'
|
|
||||||
|
|
||||||
describe(`in random page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://google.com`))
|
|
||||||
|
|
||||||
it('wait for hidden non-exist element should resolve null', async () => {
|
|
||||||
expect(
|
|
||||||
await page.waitForSelector('.non-exist-element', { hidden: true, timeout: 1000 }),
|
|
||||||
).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('wait for exist element', async () => {
|
|
||||||
expectToFind('*')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('wait for exist element', async () => {
|
|
||||||
expectToNotFind('.non-exist-element')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('wait for non-exist element reject should throw', async () => {
|
|
||||||
await expectToReject(page.waitForSelector('.non-exist-element', { timeout: 1000 }))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cases below are expected to fail to show how async test works
|
|
||||||
|
|
||||||
// // This is expected to fail!
|
|
||||||
// it('wait for non-exist element reject should not throw', async () => {
|
|
||||||
// await expect(
|
|
||||||
// page.waitForSelector('.non-exist-element', { timeout: 1000 }),
|
|
||||||
// ).rejects.not.toThrow()
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // This is expected to fail!
|
|
||||||
// it('wait for non-exist element should resolve throw', async () => {
|
|
||||||
// await expect(page.waitForSelector('.non-exist-element', { timeout: 1000 })).resolves.toThrow()
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // This is expected to fail!
|
|
||||||
// it('wait for non-exist element should resolve not throw', async () => {
|
|
||||||
// await expect(
|
|
||||||
// page.waitForSelector('.non-exist-element', { timeout: 1000 }),
|
|
||||||
// ).resolves.not.toThrow()
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { getTextContent, sleep } from '../utils'
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/GitakoExtension/test-empty`))
|
|
||||||
|
|
||||||
it('should render error message', async () => {
|
|
||||||
await sleep(5000)
|
|
||||||
|
|
||||||
expect(await getTextContent(selectors.gitako.errorMessage)).toBe(
|
|
||||||
'This project seems to be empty.',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToFind, sleep, waitForRedirect } from '../utils'
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
|
|
||||||
|
|
||||||
it('expand to target on load and after redirect', async () => {
|
|
||||||
await sleep(3000)
|
|
||||||
|
|
||||||
// Expect Gitako sidebar to have expanded src to see contents
|
|
||||||
await expectToFind(selectors.gitako.fileItemOf('src/components'))
|
|
||||||
|
|
||||||
await page.click(selectors.github.fileListItemLinkOf('components'))
|
|
||||||
await waitForRedirect()
|
|
||||||
|
|
||||||
// Expect Gitako sidebar to have expanded components and see contents
|
|
||||||
await expectToFind(selectors.gitako.fileItemOf('src/components/Gitako.tsx'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToNotFind } from '../utils'
|
|
||||||
|
|
||||||
describe(`in GitHub homepage`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com`))
|
|
||||||
|
|
||||||
it('should not render Gitako', async () => {
|
|
||||||
await expectToNotFind('.gitako-side-bar .gitako-side-bar-body-wrapper')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToFind, expectToNotFind, sleep, waitForRedirect } from '../utils'
|
|
||||||
|
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/commits/develop`))
|
|
||||||
|
|
||||||
it('should not break go back in history', async () => {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const commitLinks = await page.$$(selectors.github.commitLinks)
|
|
||||||
if (commitLinks.length < 2) throw new Error(`No enough commits`)
|
|
||||||
commitLinks[i].click()
|
|
||||||
await waitForRedirect()
|
|
||||||
await expectToFind(selectors.github.commitPage)
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
page.goBack()
|
|
||||||
await sleep(1000)
|
|
||||||
// The selector for file content
|
|
||||||
await expectToNotFind(selectors.github.commitPage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToFind, expectToNotFind, sleep, waitForRedirect } from '../utils'
|
|
||||||
|
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
|
|
||||||
|
|
||||||
it('should not break go back in history', async () => {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const fileItems = await page.$$(selectors.github.fileListItemFileLinks)
|
|
||||||
if (fileItems.length < 2) throw new Error(`No enough files`)
|
|
||||||
|
|
||||||
await waitForRedirect(async () => {
|
|
||||||
await fileItems[i].click()
|
|
||||||
})
|
|
||||||
await expectToFind(selectors.github.fileContent)
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
page.goBack()
|
|
||||||
await sleep(1000)
|
|
||||||
// The selector for file content
|
|
||||||
|
|
||||||
await expectToNotFind(selectors.github.fileContent)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import {
|
|
||||||
collapseFloatModeSidebar,
|
|
||||||
expandFloatModeSidebar,
|
|
||||||
getTextContent,
|
|
||||||
patientClick,
|
|
||||||
sleep,
|
|
||||||
waitForRedirect,
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
|
|
||||||
|
|
||||||
it('should work with PJAX', async () => {
|
|
||||||
await sleep(3000)
|
|
||||||
|
|
||||||
await expandFloatModeSidebar()
|
|
||||||
await patientClick(selectors.gitako.fileItemOf('src/analytics.ts'))
|
|
||||||
await waitForRedirect()
|
|
||||||
await collapseFloatModeSidebar()
|
|
||||||
|
|
||||||
await page.click(selectors.github.navBarItemIssues)
|
|
||||||
await waitForRedirect()
|
|
||||||
|
|
||||||
await page.click(selectors.github.navBarItemPulls)
|
|
||||||
await waitForRedirect()
|
|
||||||
|
|
||||||
page.goBack()
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
page.goBack()
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
expect(await getTextContent(selectors.github.breadcrumbFileName)).toBe('/analytics.ts')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import {
|
|
||||||
expandFloatModeSidebar,
|
|
||||||
expectToFind,
|
|
||||||
expectToNotFind,
|
|
||||||
patientClick,
|
|
||||||
sleep,
|
|
||||||
waitForRedirect,
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/test/multiple-changes`))
|
|
||||||
|
|
||||||
it('should work with PJAX', async () => {
|
|
||||||
await sleep(3000)
|
|
||||||
|
|
||||||
await expandFloatModeSidebar()
|
|
||||||
await patientClick(selectors.gitako.fileItemOf('.babelrc'))
|
|
||||||
await waitForRedirect()
|
|
||||||
|
|
||||||
await expectToFind(selectors.github.fileContent)
|
|
||||||
|
|
||||||
await waitForRedirect(async () => {
|
|
||||||
await sleep(1000) // This prevents failing in some cases due to some mystery scheduling issue of puppeteer or jest
|
|
||||||
page.goBack()
|
|
||||||
})
|
|
||||||
|
|
||||||
// The selector for file content
|
|
||||||
await expectToNotFind(selectors.github.fileContent)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expandFloatModeSidebar, expectToFind, expectToNotFind, scroll } from '../utils'
|
|
||||||
|
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() =>
|
|
||||||
page.goto(
|
|
||||||
testURL`https://github.com/EnixCoda/Gitako/tree/test/200-changed-files-200-lines-each`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it('should render Gitako', async () => {
|
|
||||||
await expectToFind(selectors.gitako.bodyWrapper)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render file list', async () => {
|
|
||||||
await expectToFind(selectors.gitako.fileItem)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render while scroll', async () => {
|
|
||||||
await expandFloatModeSidebar()
|
|
||||||
|
|
||||||
const filesEle = await page.waitForSelector(selectors.gitako.files)
|
|
||||||
// node of tsconfig.json should NOT be rendered before scroll down
|
|
||||||
await expectToNotFind(selectors.gitako.fileItemOf('tsconfig.json'))
|
|
||||||
const box = await filesEle?.boundingBox()
|
|
||||||
if (box) {
|
|
||||||
await page.mouse.move(box.x + 40, box.y + 40)
|
|
||||||
await scroll({ totalDistance: 10000, stepDistance: 100 })
|
|
||||||
|
|
||||||
// node of tsconfig.json should be rendered now
|
|
||||||
await expectToFind(selectors.gitako.fileItemOf('tsconfig.json'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { selectors } from '../selectors'
|
|
||||||
import { testURL } from '../testURL'
|
|
||||||
import { expectToFind } from '../utils'
|
|
||||||
|
|
||||||
describe(`in Gitako project page`, () => {
|
|
||||||
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/pull/71`))
|
|
||||||
|
|
||||||
it('should render Gitako', async () => {
|
|
||||||
await expectToFind(selectors.gitako.bodyWrapper)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render file list', async () => {
|
|
||||||
await expectToFind(selectors.gitako.fileItem)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
1
__tests__/global.d.ts
vendored
1
__tests__/global.d.ts
vendored
|
|
@ -1 +0,0 @@
|
||||||
declare var page: Puppeteer.Page
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
// For a detailed explanation regarding each configuration property, visit:
|
|
||||||
// https://jestjs.io/docs/en/configuration.html
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// All imported modules in your tests should be mocked automatically
|
|
||||||
// automock: false,
|
|
||||||
|
|
||||||
// Stop running tests after `n` failures
|
|
||||||
// bail: 0,
|
|
||||||
|
|
||||||
// Respect "browser" field in package.json when resolving modules
|
|
||||||
// browser: false,
|
|
||||||
|
|
||||||
// The directory where Jest should store its cached dependency information
|
|
||||||
// cacheDirectory: "/private/var/folders/6j/kp9yt47x4872_k7d4gdjbwfm0000gn/T/jest_dx",
|
|
||||||
|
|
||||||
// Automatically clear mock calls and instances between every test
|
|
||||||
// clearMocks: false,
|
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
|
||||||
// collectCoverage: false,
|
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
||||||
// collectCoverageFrom: null,
|
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
|
||||||
// coverageDirectory: null,
|
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
|
||||||
// coveragePathIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
|
||||||
// coverageReporters: [
|
|
||||||
// "json",
|
|
||||||
// "text",
|
|
||||||
// "lcov",
|
|
||||||
// "clover"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
|
||||||
// coverageThreshold: null,
|
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
|
||||||
// dependencyExtractor: null,
|
|
||||||
|
|
||||||
// Make calling deprecated APIs throw helpful error messages
|
|
||||||
// errorOnDeprecated: false,
|
|
||||||
|
|
||||||
// Force coverage collection from ignored files using an array of glob patterns
|
|
||||||
// forceCoverageMatch: [],
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once before all test suites
|
|
||||||
// globalSetup: null,
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once after all test suites
|
|
||||||
// globalTeardown: null,
|
|
||||||
|
|
||||||
// A set of global variables that need to be available in all test environments
|
|
||||||
// globals: {},
|
|
||||||
|
|
||||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
||||||
maxWorkers: 1,
|
|
||||||
|
|
||||||
// An array of directory names to be searched recursively up from the requiring module's location
|
|
||||||
// moduleDirectories: [
|
|
||||||
// "node_modules"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of file extensions your modules use
|
|
||||||
// moduleFileExtensions: [
|
|
||||||
// "js",
|
|
||||||
// "json",
|
|
||||||
// "jsx",
|
|
||||||
// "ts",
|
|
||||||
// "tsx",
|
|
||||||
// "node"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
|
||||||
// moduleNameMapper: {},
|
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
||||||
// modulePathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Activates notifications for test results
|
|
||||||
// notify: false,
|
|
||||||
|
|
||||||
// An enum that specifies notification mode. Requires { notify: true }
|
|
||||||
// notifyMode: "failure-change",
|
|
||||||
|
|
||||||
// A preset that is used as a base for Jest's configuration
|
|
||||||
preset: 'jest-puppeteer',
|
|
||||||
|
|
||||||
// Run tests from one or more projects
|
|
||||||
// projects: null,
|
|
||||||
|
|
||||||
// Use this configuration option to add custom reporters to Jest
|
|
||||||
// reporters: undefined,
|
|
||||||
|
|
||||||
// Automatically reset mock state between every test
|
|
||||||
// resetMocks: false,
|
|
||||||
|
|
||||||
// Reset the module registry before running each individual test
|
|
||||||
// resetModules: false,
|
|
||||||
|
|
||||||
// A path to a custom resolver
|
|
||||||
// resolver: null,
|
|
||||||
|
|
||||||
// Automatically restore mock state between every test
|
|
||||||
// restoreMocks: false,
|
|
||||||
|
|
||||||
// The root directory that Jest should scan for tests and modules within
|
|
||||||
// rootDir: null,
|
|
||||||
|
|
||||||
// A list of paths to directories that Jest should use to search for files in
|
|
||||||
// roots: [
|
|
||||||
// "<rootDir>"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Allows you to use a custom runner instead of Jest's default test runner
|
|
||||||
// runner: "jest-runner",
|
|
||||||
|
|
||||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
||||||
// setupFiles: [],
|
|
||||||
|
|
||||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
||||||
// setupFilesAfterEnv: [],
|
|
||||||
|
|
||||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
||||||
// snapshotSerializers: [],
|
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
|
||||||
// testEnvironment: 'node',
|
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
|
||||||
// testEnvironmentOptions: {},
|
|
||||||
|
|
||||||
// Adds a location field to test results
|
|
||||||
// testLocationInResults: false,
|
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
|
||||||
testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
||||||
testPathIgnorePatterns: ['/node_modules/', '.d.ts$'],
|
|
||||||
|
|
||||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
||||||
// testRegex: [],
|
|
||||||
|
|
||||||
// This option allows the use of a custom results processor
|
|
||||||
// testResultsProcessor: null,
|
|
||||||
|
|
||||||
// This option allows use of a custom test runner
|
|
||||||
// testRunner: "jasmine2",
|
|
||||||
|
|
||||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
|
||||||
// testURL: "http://localhost",
|
|
||||||
|
|
||||||
testTimeout: 30 * 1000,
|
|
||||||
|
|
||||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
|
||||||
// timers: "real",
|
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
|
||||||
// transform: null,
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
||||||
// transformIgnorePatterns: [
|
|
||||||
// "/node_modules/"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
||||||
// unmockedModulePathPatterns: undefined,
|
|
||||||
|
|
||||||
// Indicates whether each individual test should be reported during the run
|
|
||||||
// verbose: null,
|
|
||||||
|
|
||||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
||||||
// watchPathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Whether to use watchman for file crawling
|
|
||||||
// watchman: true,
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
const baseConfig = require('./jest.config')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...baseConfig,
|
|
||||||
testMatch: [...baseConfig.testMatch, '**/__tests__/cases/**/*.ts?(x)'],
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/setup.ts'],
|
|
||||||
}
|
|
||||||
4
__tests__/puppeteer.d.ts
vendored
4
__tests__/puppeteer.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
import * as Puppeteer from 'puppeteer'
|
|
||||||
|
|
||||||
export as namespace Puppeteer
|
|
||||||
export = Puppeteer
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
jest.retryTimes(3)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"include": ["."],
|
|
||||||
"exclude": ["tsconfig.json"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES5",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"baseUrl": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
export async function expectToResolve<T>(promise: Promise<T>) {
|
|
||||||
const pass = jest.fn()
|
|
||||||
await promise.then(pass)
|
|
||||||
expect(pass).toHaveBeenCalled()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectToReject<T>(promise: Promise<T>) {
|
|
||||||
const pass = jest.fn()
|
|
||||||
await promise.catch(pass)
|
|
||||||
expect(pass).toHaveBeenCalled()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectToFind(selector: string) {
|
|
||||||
await expectToResolve(page.waitForSelector(selector, { timeout: 2000 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectToNotFind(selector: string) {
|
|
||||||
await expectToReject(page.waitForSelector(selector, { timeout: 1000 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sleep(timeout: number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, timeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function scroll({
|
|
||||||
totalDistance,
|
|
||||||
stepDistance = 100,
|
|
||||||
}: {
|
|
||||||
totalDistance: number
|
|
||||||
stepDistance?: number
|
|
||||||
}) {
|
|
||||||
let distance = 0
|
|
||||||
while ((distance += stepDistance) < totalDistance) {
|
|
||||||
await page.mouse.wheel({ deltaY: stepDistance })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assert(condition: boolean, err?: Error | string): asserts condition {
|
|
||||||
if (!condition) throw typeof err === 'string' ? new Error(err) : err
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter = 0
|
|
||||||
export async function listenTo(
|
|
||||||
event: string,
|
|
||||||
target: 'document' | 'window',
|
|
||||||
callback: <Args extends unknown[]>(...args: Args) => void,
|
|
||||||
oneTime?: boolean,
|
|
||||||
) {
|
|
||||||
const callbackName = 'onEvent' + ++counter
|
|
||||||
await page.exposeFunction(callbackName, callback)
|
|
||||||
await page.evaluate(
|
|
||||||
(event: string, target: 'window' | 'document', callbackName: string, oneTime?: boolean) => {
|
|
||||||
const t = target === 'document' ? document : window
|
|
||||||
const onEvent = (...args: unknown[]): void => {
|
|
||||||
const method = window[callbackName as keyof Window]
|
|
||||||
method?.(...args)
|
|
||||||
if (oneTime) t.removeEventListener(event, onEvent)
|
|
||||||
}
|
|
||||||
t.addEventListener(event, onEvent)
|
|
||||||
},
|
|
||||||
event,
|
|
||||||
target,
|
|
||||||
callbackName,
|
|
||||||
oneTime || false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function once(event: string, target: 'document' | 'window') {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
listenTo(
|
|
||||||
event,
|
|
||||||
target,
|
|
||||||
(...args) => {
|
|
||||||
resolve(args)
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForLegacyPJAXRedirect(action?: () => void | Promise<void>) {
|
|
||||||
const promise = once('pjax:end', 'document')
|
|
||||||
await action?.()
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForTurboRedirect(action?: () => void | Promise<void>) {
|
|
||||||
const promise = once('turbo:load', 'document')
|
|
||||||
await action?.()
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForRedirect(action?: () => void | Promise<void>) {
|
|
||||||
let fired = false
|
|
||||||
const $action =
|
|
||||||
action &&
|
|
||||||
(() => {
|
|
||||||
if (fired) return
|
|
||||||
fired = true
|
|
||||||
return action()
|
|
||||||
})
|
|
||||||
return Promise.race([
|
|
||||||
waitForLegacyPJAXRedirect($action),
|
|
||||||
waitForTurboRedirect($action),
|
|
||||||
sleep(3 * 1000),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function patientClick(selector: string) {
|
|
||||||
await page.waitForSelector(selector)
|
|
||||||
await page.click(selector)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expandFloatModeSidebar() {
|
|
||||||
const rect = await (
|
|
||||||
await page.$('.gitako-toggle-show-button')
|
|
||||||
)?.evaluate(button => {
|
|
||||||
const { x, y, width, height } = button.getBoundingClientRect()
|
|
||||||
// pass required properties to avoid serialization issues
|
|
||||||
return { x, y, width, height }
|
|
||||||
})
|
|
||||||
if (rect) {
|
|
||||||
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
|
||||||
await sleep(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function collapseFloatModeSidebar() {
|
|
||||||
await page.mouse.move(600, 600, {
|
|
||||||
steps: 100,
|
|
||||||
})
|
|
||||||
await sleep(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTextContent(query: string) {
|
|
||||||
return page.evaluate(query => document.querySelector(query)?.textContent, query)
|
|
||||||
}
|
|
||||||
30
e2e/baseline.spec.ts
Normal file
30
e2e/baseline.spec.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Confirm basic behaviors of playwright assertions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
|
||||||
|
test.describe('in random page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://google.com`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('wait for hidden non-exist element should resolve null', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator('.non-exist-element')).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('wait for exist element', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator('*').first()).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('wait for non-exist element should not be visible', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator('.non-exist-element')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('wait for non-exist element should timeout', async ({ extensionPage }) => {
|
||||||
|
await expect(async () => {
|
||||||
|
await extensionPage.waitForSelector('.non-exist-element', { timeout: 1000 })
|
||||||
|
}).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
17
e2e/empty-project.spec.ts
Normal file
17
e2e/empty-project.spec.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { getTextContent, sleep } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako empty project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/GitakoExtension/test-empty`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render error message', async ({ extensionPage }) => {
|
||||||
|
await sleep(5000)
|
||||||
|
|
||||||
|
const textContent = await getTextContent(extensionPage, selectors.gitako.errorMessage)
|
||||||
|
expect(textContent).toBe('This project seems to be empty.')
|
||||||
|
})
|
||||||
|
})
|
||||||
27
e2e/expand-to-target.spec.ts
Normal file
27
e2e/expand-to-target.spec.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { sleep, waitForRedirect } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('expand to target on load and after redirect', async ({ extensionPage }) => {
|
||||||
|
await sleep(3000)
|
||||||
|
|
||||||
|
// Expect Gitako sidebar to have expanded src to see contents
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.fileItemOf('src/components'))).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
|
||||||
|
await extensionPage.click(selectors.github.fileListItemLinkOf('components'))
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
|
||||||
|
// Expect Gitako sidebar to have expanded components and see contents
|
||||||
|
await expect(
|
||||||
|
extensionPage.locator(selectors.gitako.fileItemOf('src/components/Gitako.tsx')),
|
||||||
|
).toBeVisible({ timeout: 5000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
29
e2e/fixtures.ts
Normal file
29
e2e/fixtures.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { test as base, chromium, type BrowserContext, type Page } from '@playwright/test'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const EXTENSION_PATH = path.resolve(__dirname, '..', 'dist')
|
||||||
|
|
||||||
|
export const test = base.extend<{
|
||||||
|
context: BrowserContext
|
||||||
|
extensionPage: Page
|
||||||
|
}>({
|
||||||
|
// eslint-disable-next-line no-empty-pattern
|
||||||
|
context: async ({}, use) => {
|
||||||
|
const context = await chromium.launchPersistentContext('', {
|
||||||
|
headless: false,
|
||||||
|
args: [
|
||||||
|
`--no-sandbox`,
|
||||||
|
`--disable-extensions-except=${EXTENSION_PATH}`,
|
||||||
|
`--load-extension=${EXTENSION_PATH}`,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await use(context)
|
||||||
|
await context.close()
|
||||||
|
},
|
||||||
|
extensionPage: async ({ context }, use) => {
|
||||||
|
const page = await context.newPage()
|
||||||
|
await use(page)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const expect = test.expect
|
||||||
14
e2e/homepage.not-render.spec.ts
Normal file
14
e2e/homepage.not-render.spec.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
|
||||||
|
test.describe('in GitHub homepage', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not render Gitako', async ({ extensionPage }) => {
|
||||||
|
await expect(
|
||||||
|
extensionPage.locator('.gitako-side-bar .gitako-side-bar-body-wrapper'),
|
||||||
|
).not.toBeVisible({ timeout: 2000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
31
e2e/pjax.commits-page.spec.ts
Normal file
31
e2e/pjax.commits-page.spec.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { sleep, waitForRedirect } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako commits page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/commits/develop`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not break go back in history', async ({ extensionPage }) => {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const commitLinks = await extensionPage.locator(selectors.github.commitLinks).all()
|
||||||
|
if (commitLinks.length < 2) throw new Error(`No enough commits`)
|
||||||
|
|
||||||
|
await commitLinks[i].click()
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
await expect(extensionPage.locator(selectors.github.commitPage).first()).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
await extensionPage.goBack()
|
||||||
|
await sleep(1000)
|
||||||
|
// The selector for commit page should not be visible
|
||||||
|
await expect(extensionPage.locator(selectors.github.commitPage)).not.toBeVisible({
|
||||||
|
timeout: 2000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
32
e2e/pjax.files-page.spec.ts
Normal file
32
e2e/pjax.files-page.spec.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { sleep, waitForRedirect } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not break go back in history', async ({ extensionPage }) => {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const fileItems = await extensionPage.locator(selectors.github.fileListItemFileLinks).all()
|
||||||
|
if (fileItems.length < 2) throw new Error(`No enough files`)
|
||||||
|
|
||||||
|
await waitForRedirect(extensionPage, async () => {
|
||||||
|
await fileItems[i].click()
|
||||||
|
})
|
||||||
|
await expect(extensionPage.locator(selectors.github.fileContent)).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
await extensionPage.goBack()
|
||||||
|
await sleep(1000)
|
||||||
|
// The selector for file content should not be visible
|
||||||
|
await expect(extensionPage.locator(selectors.github.fileContent)).not.toBeVisible({
|
||||||
|
timeout: 2000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
41
e2e/pjax.general.spec.ts
Normal file
41
e2e/pjax.general.spec.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import {
|
||||||
|
collapseFloatModeSidebar,
|
||||||
|
expandFloatModeSidebar,
|
||||||
|
getTextContent,
|
||||||
|
patientClick,
|
||||||
|
sleep,
|
||||||
|
waitForRedirect,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work with PJAX', async ({ extensionPage }) => {
|
||||||
|
await sleep(3000)
|
||||||
|
|
||||||
|
await expandFloatModeSidebar(extensionPage)
|
||||||
|
await patientClick(extensionPage, selectors.gitako.fileItemOf('src/analytics.ts'))
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
await collapseFloatModeSidebar(extensionPage)
|
||||||
|
|
||||||
|
await extensionPage.click(selectors.github.navBarItemIssues)
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
|
||||||
|
await extensionPage.click(selectors.github.navBarItemPulls)
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
|
||||||
|
await extensionPage.goBack()
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
await extensionPage.goBack()
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
const textContent = await getTextContent(extensionPage, selectors.github.breadcrumbFileName)
|
||||||
|
expect(textContent).toBe('/analytics.ts')
|
||||||
|
})
|
||||||
|
})
|
||||||
30
e2e/pjax.internal.spec.ts
Normal file
30
e2e/pjax.internal.spec.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { expandFloatModeSidebar, patientClick, sleep, waitForRedirect } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/tree/test/multiple-changes`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work with PJAX', async ({ extensionPage }) => {
|
||||||
|
await sleep(3000)
|
||||||
|
|
||||||
|
await expandFloatModeSidebar(extensionPage)
|
||||||
|
await patientClick(extensionPage, selectors.gitako.fileItemOf('.babelrc'))
|
||||||
|
await waitForRedirect(extensionPage)
|
||||||
|
|
||||||
|
await expect(extensionPage.locator(selectors.github.fileContent)).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
|
await waitForRedirect(extensionPage, async () => {
|
||||||
|
await sleep(1000) // This prevents failing in some cases due to some mystery scheduling issue
|
||||||
|
await extensionPage.goBack()
|
||||||
|
})
|
||||||
|
|
||||||
|
// The selector for file content should not be visible
|
||||||
|
await expect(extensionPage.locator(selectors.github.fileContent)).not.toBeVisible({
|
||||||
|
timeout: 2000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
46
e2e/project-page.gitako.spec.ts
Normal file
46
e2e/project-page.gitako.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
import { expandFloatModeSidebar, scroll } from './utils'
|
||||||
|
|
||||||
|
test.describe('in Gitako project page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(
|
||||||
|
testURL`https://github.com/EnixCoda/Gitako/tree/test/200-changed-files-200-lines-each`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render Gitako', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.bodyWrapper)).toBeVisible({ timeout: 5000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render file list', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.fileItem).first()).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render while scroll', async ({ extensionPage }) => {
|
||||||
|
await expandFloatModeSidebar(extensionPage)
|
||||||
|
|
||||||
|
await extensionPage.waitForSelector(selectors.gitako.files)
|
||||||
|
// node of tsconfig.json should NOT be rendered before scroll down
|
||||||
|
await expect(
|
||||||
|
extensionPage.locator(selectors.gitako.fileItemOf('tsconfig.json')),
|
||||||
|
).not.toBeVisible({
|
||||||
|
timeout: 2000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const filesEle = extensionPage.locator(selectors.gitako.files)
|
||||||
|
const box = await filesEle.boundingBox()
|
||||||
|
if (box) {
|
||||||
|
await extensionPage.mouse.move(box.x + 40, box.y + 40)
|
||||||
|
await scroll(extensionPage, { totalDistance: 10000, stepDistance: 100 })
|
||||||
|
|
||||||
|
// node of tsconfig.json should be rendered now
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.fileItemOf('tsconfig.json'))).toBeVisible(
|
||||||
|
{ timeout: 5000 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
19
e2e/pull-request-page.gitako.spec.ts
Normal file
19
e2e/pull-request-page.gitako.spec.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { expect, test } from './fixtures'
|
||||||
|
import { selectors } from './selectors'
|
||||||
|
import { testURL } from './testURL'
|
||||||
|
|
||||||
|
test.describe('in Gitako pull request page', () => {
|
||||||
|
test.beforeEach(async ({ extensionPage }) => {
|
||||||
|
await extensionPage.goto(testURL`https://github.com/EnixCoda/Gitako/pull/71`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render Gitako', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.bodyWrapper)).toBeVisible({ timeout: 5000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should render file list', async ({ extensionPage }) => {
|
||||||
|
await expect(extensionPage.locator(selectors.gitako.fileItem).first()).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
15
e2e/tsconfig.json
Normal file
15
e2e/tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"strict": true,
|
||||||
|
"lib": ["dom", "es2017.object", "es2016", "ES2019.Array", "ES2020.String", "ES2022.Error"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts"]
|
||||||
|
}
|
||||||
98
e2e/utils.ts
Normal file
98
e2e/utils.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export function sleep(timeout: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scroll(
|
||||||
|
page: Page,
|
||||||
|
{
|
||||||
|
totalDistance,
|
||||||
|
stepDistance = 100,
|
||||||
|
}: {
|
||||||
|
totalDistance: number
|
||||||
|
stepDistance?: number
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
let distance = 0
|
||||||
|
while ((distance += stepDistance) < totalDistance) {
|
||||||
|
await page.mouse.wheel(0, stepDistance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assert(condition: boolean, err?: Error | string): asserts condition {
|
||||||
|
if (!condition) throw typeof err === 'string' ? new Error(err) : err
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForLegacyPJAXRedirect(page: Page, action?: () => void | Promise<void>) {
|
||||||
|
const promise = page.evaluate(() => {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
const onEvent = () => {
|
||||||
|
document.removeEventListener('pjax:end', onEvent)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
document.addEventListener('pjax:end', onEvent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await action?.()
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForTurboRedirect(page: Page, action?: () => void | Promise<void>) {
|
||||||
|
const promise = page.evaluate(() => {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
const onEvent = () => {
|
||||||
|
document.removeEventListener('turbo:load', onEvent)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
document.addEventListener('turbo:load', onEvent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await action?.()
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForRedirect(page: Page, action?: () => void | Promise<void>) {
|
||||||
|
let fired = false
|
||||||
|
const $action =
|
||||||
|
action &&
|
||||||
|
(() => {
|
||||||
|
if (fired) return
|
||||||
|
fired = true
|
||||||
|
return action()
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.race([
|
||||||
|
waitForLegacyPJAXRedirect(page, $action),
|
||||||
|
waitForTurboRedirect(page, $action),
|
||||||
|
sleep(3 * 1000),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patientClick(page: Page, selector: string) {
|
||||||
|
await page.waitForSelector(selector)
|
||||||
|
await page.click(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expandFloatModeSidebar(page: Page) {
|
||||||
|
const button = page.locator('.gitako-toggle-show-button')
|
||||||
|
const rect = await button.boundingBox()
|
||||||
|
if (rect) {
|
||||||
|
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||||
|
await sleep(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collapseFloatModeSidebar(page: Page) {
|
||||||
|
await page.mouse.move(600, 600, {
|
||||||
|
steps: 100,
|
||||||
|
})
|
||||||
|
await sleep(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTextContent(
|
||||||
|
page: Page,
|
||||||
|
query: string,
|
||||||
|
): Promise<string | null | undefined> {
|
||||||
|
return page.evaluate((query: string) => document.querySelector(query)?.textContent, query)
|
||||||
|
}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
const path = require('path')
|
|
||||||
if (process.arch === 'arm64' && process.platform === 'darwin') {
|
|
||||||
require('dotenv').config()
|
|
||||||
}
|
|
||||||
|
|
||||||
const CRX_PATH = path.resolve(__dirname, 'dist')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
launch: {
|
|
||||||
// set by mujo-code/puppeteer-headful on GitHub actions
|
|
||||||
// also for usages on ARM chip Mac
|
|
||||||
executablePath: process.env.PUPPETEER_EXEC_PATH,
|
|
||||||
// required for enabling extensions
|
|
||||||
headless: false,
|
|
||||||
args: [
|
|
||||||
`--no-sandbox`,
|
|
||||||
`--disable-extensions-except=${CRX_PATH}`,
|
|
||||||
`--load-extension=${CRX_PATH}`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
"build": "VERSION=v$(node scripts/get-version.js) NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack",
|
"build": "VERSION=v$(node scripts/get-version.js) NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack",
|
||||||
"build:all": "GITAKO_TARGET= yarn run build",
|
"build:all": "GITAKO_TARGET= yarn run build",
|
||||||
"build:analyze": "ANALYZE= yarn run build",
|
"build:analyze": "ANALYZE= yarn run build",
|
||||||
"test": "NODE_ENV=test jest --config __tests__/jest.puppeteer.config.js",
|
"test": "playwright test",
|
||||||
|
"test:ui": "playwright test --ui",
|
||||||
"test:unit": "NODE_ENV=test jest --config jest.config.js"
|
"test:unit": "NODE_ENV=test jest --config jest.config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -51,8 +52,10 @@
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"@babel/preset-typescript": "^7.16.7",
|
"@babel/preset-typescript": "^7.16.7",
|
||||||
|
"@playwright/test": "^1.40.0",
|
||||||
"@sentry/cli": "^1.64.2",
|
"@sentry/cli": "^1.64.2",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
|
"@types/dotenv": "^6",
|
||||||
"@types/firefox-webext-browser": "^120.0.3",
|
"@types/firefox-webext-browser": "^120.0.3",
|
||||||
"@types/history": "^5.0.0",
|
"@types/history": "^5.0.0",
|
||||||
"@types/ini": "^1.3.31",
|
"@types/ini": "^1.3.31",
|
||||||
|
|
@ -60,7 +63,6 @@
|
||||||
"@types/js-base64": "^3.3.1",
|
"@types/js-base64": "^3.3.1",
|
||||||
"@types/node": "^18",
|
"@types/node": "^18",
|
||||||
"@types/nprogress": "^0.0.29",
|
"@types/nprogress": "^0.0.29",
|
||||||
"@types/puppeteer": "^5.4.3",
|
|
||||||
"@types/react": "^18.0.9",
|
"@types/react": "^18.0.9",
|
||||||
"@types/react-dom": "^18.0.3",
|
"@types/react-dom": "^18.0.3",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
|
|
@ -83,13 +85,11 @@
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-puppeteer": "^10.0.1",
|
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"mini-css-extract-plugin": "^2.9.0",
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.3",
|
||||||
"puppeteer": "^22.12.1",
|
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
"sass": "^1.26.2",
|
"sass": "^1.26.2",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
|
|
|
||||||
45
playwright.config.mts
Normal file
45
playwright.config.mts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
if (process.arch === 'arm64' && process.platform === 'darwin') {
|
||||||
|
dotenv.config()
|
||||||
|
}
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const EXTENSION_PATH = path.resolve(__dirname, 'dist')
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 3 : 0,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
timeout: 60000,
|
||||||
|
expect: {
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
video: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
// Load the extension
|
||||||
|
launchOptions: {
|
||||||
|
args: [
|
||||||
|
`--no-sandbox`,
|
||||||
|
`--disable-extensions-except=${EXTENSION_PATH}`,
|
||||||
|
`--load-extension=${EXTENSION_PATH}`,
|
||||||
|
],
|
||||||
|
headless: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { promises: fs, existsSync } = require('fs')
|
const { promises: fs, existsSync } = require('fs')
|
||||||
const puppeteer = require('puppeteer')
|
const { chromium } = require('@playwright/test')
|
||||||
const generateFileIconIndex = require('./generate-file-icon-index')
|
const generateFileIconIndex = require('./generate-file-icon-index')
|
||||||
const generateFolderIconIndex = require('./generate-folder-icon-index')
|
const generateFolderIconIndex = require('./generate-folder-icon-index')
|
||||||
const { emitDirPath, checkEmitDir } = require('./check-emit-dir')
|
const { emitDirPath, checkEmitDir } = require('./check-emit-dir')
|
||||||
|
|
@ -8,8 +8,9 @@ const { emitDirPath, checkEmitDir } = require('./check-emit-dir')
|
||||||
let browser
|
let browser
|
||||||
async function getPage() {
|
async function getPage() {
|
||||||
const headless = process.env.HEADLESS !== 'false'
|
const headless = process.env.HEADLESS !== 'false'
|
||||||
browser = browser || (await puppeteer.launch({ headless }))
|
browser = browser || (await chromium.launch({ headless }))
|
||||||
return await browser.newPage()
|
const context = await browser.newContext()
|
||||||
|
return await context.newPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateCSV() {
|
async function generateCSV() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue