mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
Compare commits
25 commits
release-v3
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954a132624 | ||
|
|
b4ace1f54b | ||
|
|
6e193db9c5 | ||
|
|
750f8e4462 | ||
|
|
d149a4903d | ||
|
|
5352526d74 | ||
|
|
7560895fff | ||
|
|
5a687cb9f5 | ||
|
|
fd3476c1fa | ||
|
|
68915bfac4 | ||
|
|
08bb716ef4 | ||
|
|
6ac043a13a | ||
|
|
420e1e288e | ||
|
|
6331892329 | ||
|
|
3cf4deaa9f | ||
|
|
550b3f0d41 | ||
|
|
ecaf9087a3 | ||
|
|
f67065bcf4 | ||
|
|
4bcce483e9 | ||
|
|
a9979620f4 | ||
|
|
ba06289ed3 | ||
|
|
30668b16aa | ||
|
|
225a944685 | ||
|
|
697b6e2ad4 | ||
|
|
186e1365d0 |
77 changed files with 3051 additions and 3435 deletions
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"es2022": true
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["webpack.config.ts", "*.tsx?"],
|
|
||||||
"excludedFiles": ["*.d.ts"],
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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,4 +1,4 @@
|
||||||
*-profile/
|
*-profile/
|
||||||
dist/
|
dist/
|
||||||
vscode-icons/
|
/vscode-icons
|
||||||
Safari
|
Safari
|
||||||
|
|
|
||||||
20
.swcrc
Normal file
20
.swcrc
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://swc.rs/schema.json",
|
||||||
|
"jsc": {
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"decorators": false,
|
||||||
|
"dynamicImport": true
|
||||||
|
},
|
||||||
|
"transform": {
|
||||||
|
"react": {
|
||||||
|
"runtime": "automatic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": "es2022"
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ Gitako is a free file tree extension for GitHub, available on Chrome, Firefox an
|
||||||
|
|
||||||
<a href="https://chrome.google.com/webstore/detail/gitako-github-file-tree/giljefjcheohhamkjphiebfjnlphnokk"><img width="64" alt="Chrome" src="./assets/Chrome.svg" /></a>
|
<a href="https://chrome.google.com/webstore/detail/gitako-github-file-tree/giljefjcheohhamkjphiebfjnlphnokk"><img width="64" alt="Chrome" src="./assets/Chrome.svg" /></a>
|
||||||
<a href="https://microsoftedge.microsoft.com/addons/detail/alpoloddcggjhakjemghahlkofjekbca"><img width="64" alt="Edge" src="./assets/Edge.svg" /></a>
|
<a href="https://microsoftedge.microsoft.com/addons/detail/alpoloddcggjhakjemghahlkofjekbca"><img width="64" alt="Edge" src="./assets/Edge.svg" /></a>
|
||||||
<a href="https://addons.mozilla.org/en-US/firefox/addon/gitako-github-file-tree/"><img width="64" alt="Firefox" src="./assets/Firefox.svg" /></a>
|
<a href="https://addons.mozilla.org/firefox/addon/gitako-github-file-tree/"><img width="64" alt="Firefox" src="./assets/Firefox.svg" /></a>
|
||||||
|
|
||||||
It is more recommended for Edge users to install from Chrome store. It may delay for weeks before updates got published to Edge store because its review process is slow.
|
It is more recommended for Edge users to install from Chrome store. It may delay for weeks before updates got published to Edge store because its review process is slow.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// `.babelrc` is not loaded by babel-loader for files under node_modules, but `babel.config.js` is
|
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
test: {
|
|
||||||
plugins: ['babel-plugin-transform-es2015-modules-commonjs'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
modules: false,
|
|
||||||
targets: {
|
|
||||||
esmodules: true,
|
|
||||||
},
|
|
||||||
exclude: [
|
|
||||||
'@babel/plugin-transform-async-to-generator',
|
|
||||||
'@babel/plugin-proposal-object-rest-spread',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
'@babel/preset-react',
|
|
||||||
],
|
|
||||||
plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-class-properties'],
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
}
|
||||||
98
eslint.config.mjs
Normal file
98
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import eslint from '@eslint/js'
|
||||||
|
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import globals from 'globals'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist*/',
|
||||||
|
'node_modules/',
|
||||||
|
'Safari/',
|
||||||
|
'vscode-icons/',
|
||||||
|
'server/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'playwright-report/',
|
||||||
|
'test-results/',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.es2022,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-hooks/rules-of-hooks': 'off', // for IIFC
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['e2e/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['scripts/**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['scripts/vscode-icons/**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.config.{js,ts,cjs,cts}', '*.{cjs,cts}'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -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}`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -142,7 +142,7 @@ module.exports = {
|
||||||
testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
|
testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
testPathIgnorePatterns: ['/node_modules/', '.d.ts$', '<rootDir>/vscode-icons/'],
|
testPathIgnorePatterns: ['/node_modules/', '.d.ts$', '<rootDir>/vscode-icons/', '<rootDir>/e2e/'],
|
||||||
|
|
||||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
// testRegex: [],
|
// testRegex: [],
|
||||||
|
|
@ -162,12 +162,27 @@ module.exports = {
|
||||||
// timers: "real",
|
// timers: "real",
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
// A map from regular expressions to paths to transformers
|
||||||
// transform: null,
|
transform: {
|
||||||
|
'^.+\\.(t|j)sx?$': [
|
||||||
|
'@swc/jest',
|
||||||
|
{
|
||||||
|
jsc: {
|
||||||
|
parser: {
|
||||||
|
syntax: 'typescript',
|
||||||
|
tsx: true,
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
react: {
|
||||||
|
runtime: 'automatic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
// transformIgnorePatterns: [
|
transformIgnorePatterns: ['/node_modules/(?!(webext-.*|superstruct)/)'],
|
||||||
// "/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
|
// 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,
|
// unmockedModulePathPatterns: undefined,
|
||||||
64
package.json
64
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gitako",
|
"name": "gitako",
|
||||||
"version": "3.13.1",
|
"version": "3.15.4",
|
||||||
"description": "File tree for GitHub, and more than that.",
|
"description": "File tree for GitHub, and more than that.",
|
||||||
"repository": "https://github.com/EnixCoda/Gitako",
|
"repository": "https://github.com/EnixCoda/Gitako",
|
||||||
"author": "EnixCoda",
|
"author": "EnixCoda",
|
||||||
|
|
@ -11,17 +11,18 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "VERSION=dev-v$(node scripts/get-version.js) NODE_OPTIONS=--openssl-legacy-provider webpack-dashboard -- webpack --watch",
|
"dev": "VERSION=dev-v$(node scripts/get-version.js) webpack-dashboard -- webpack --watch",
|
||||||
"dev:all": "GITAKO_TARGET= yarn run dev",
|
"dev:all": "GITAKO_TARGET= yarn run dev",
|
||||||
"debug-firefox": "web-ext run --source-dir=dist-firefox --keep-profile-changes --start-url https://github.com/EnixCoda/Gitako",
|
"debug-firefox": "web-ext run --source-dir=dist-firefox --keep-profile-changes --start-url https://github.com/EnixCoda/Gitako",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"postversion": "sh scripts/post-version.sh",
|
"postversion": "sh scripts/post-version.sh",
|
||||||
"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_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:unit": "NODE_ENV=test jest --config jest.config.js"
|
"test:ui": "playwright test --ui",
|
||||||
|
"test:unit": "NODE_ENV=test jest --config jest.config.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primer/css": "^20.4.3",
|
"@primer/css": "^20.4.3",
|
||||||
|
|
@ -44,15 +45,12 @@
|
||||||
"webextension-polyfill": "^0.11.0"
|
"webextension-polyfill": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.17.6",
|
"@playwright/test": "^1.40.0",
|
||||||
"@babel/core": "^7.17.9",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
|
|
||||||
"@babel/preset-env": "^7.16.11",
|
|
||||||
"@babel/preset-react": "^7.16.7",
|
|
||||||
"@babel/preset-typescript": "^7.16.7",
|
|
||||||
"@sentry/cli": "^1.64.2",
|
"@sentry/cli": "^1.64.2",
|
||||||
|
"@swc/core": "^1.15.8",
|
||||||
|
"@swc/jest": "^0.2.39",
|
||||||
"@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,46 +58,44 @@
|
||||||
"@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",
|
||||||
"@types/styled-components": "^5.1.25",
|
"@types/styled-components": "^5.1.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||||
"@typescript-eslint/parser": "^5.33.1",
|
"@typescript-eslint/parser": "^8.28.0",
|
||||||
"babel-loader": "^8.2.5",
|
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"css-loader": "^2.1.0",
|
"css-loader": "^7.1.2",
|
||||||
"dotenv": "^6.2.0",
|
"dotenv": "^6.2.0",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"dotenv-webpack": "^8.1.0",
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^9.39.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^10.1.0",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^4.5.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "^6.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"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.10.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^3.8.0",
|
||||||
"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": "^16.0.6",
|
||||||
|
"swc-loader": "^0.2.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.9.3",
|
||||||
"url-loader": "^1.1.2",
|
"typescript-eslint": "^8.53.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
"web-ext": "^7.11.0",
|
"web-ext": "^7.11.0",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.104.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-dashboard": "^3.3.8"
|
"webpack-dashboard": "^3.3.8"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
|
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/rules-of-hooks": "off" // for IIFC
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
src/components/FileExplorer/DiffIcon.tsx
Normal file
21
src/components/FileExplorer/DiffIcon.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {
|
||||||
|
DiffAddedIcon,
|
||||||
|
DiffIgnoredIcon,
|
||||||
|
DiffModifiedIcon,
|
||||||
|
DiffRemovedIcon,
|
||||||
|
DiffRenamedIcon,
|
||||||
|
} from '@primer/octicons-react'
|
||||||
|
import React from 'react'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
added: DiffAddedIcon,
|
||||||
|
ignored: DiffIgnoredIcon,
|
||||||
|
modified: DiffModifiedIcon,
|
||||||
|
removed: DiffRemovedIcon,
|
||||||
|
renamed: DiffRenamedIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DiffIcon: React.FC<{
|
||||||
|
diff: Required<TreeNode>['diff']
|
||||||
|
}> = ({ diff: { status } }) => <Icon className={status} IconComponent={iconMap[status]} />
|
||||||
|
|
@ -1,27 +1,8 @@
|
||||||
import {
|
|
||||||
DiffAddedIcon,
|
|
||||||
DiffIgnoredIcon,
|
|
||||||
DiffModifiedIcon,
|
|
||||||
DiffRemovedIcon,
|
|
||||||
DiffRenamedIcon,
|
|
||||||
} from '@primer/octicons-react'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { resolveDiffGraphMeta } from 'utils/general'
|
import { resolveDiffGraphMeta } from 'utils/general'
|
||||||
import { Icon } from '../Icon'
|
|
||||||
|
|
||||||
const iconMap = {
|
export function DiffStatGraph({ diff }: { diff: Required<TreeNode>['diff'] }) {
|
||||||
added: DiffAddedIcon,
|
const { changes, additions, deletions } = diff
|
||||||
ignored: DiffIgnoredIcon,
|
|
||||||
modified: DiffModifiedIcon,
|
|
||||||
removed: DiffRemovedIcon,
|
|
||||||
renamed: DiffRenamedIcon,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiffStatGraph({
|
|
||||||
diff: { status, changes, additions, deletions },
|
|
||||||
}: {
|
|
||||||
diff: Required<TreeNode>['diff']
|
|
||||||
}) {
|
|
||||||
const { g, r, w } = resolveDiffGraphMeta(additions, deletions, changes)
|
const { g, r, w } = resolveDiffGraphMeta(additions, deletions, changes)
|
||||||
|
|
||||||
const children: React.ReactNode[] = []
|
const children: React.ReactNode[] = []
|
||||||
|
|
@ -32,10 +13,5 @@ export function DiffStatGraph({
|
||||||
for (let i = 0; i < w; i++)
|
for (let i = 0; i < w; i++)
|
||||||
children.push(<span key={`w-${i}`} className="diff-stat-graph-no-change" />)
|
children.push(<span key={`w-${i}`} className="diff-stat-graph-no-change" />)
|
||||||
|
|
||||||
return (
|
return <span className={'diff-stat-graph'}>{children}</span>
|
||||||
<span className={'diff-stat-graph'}>
|
|
||||||
<Icon className={status} IconComponent={iconMap[status]} />
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Icon } from '../Icon'
|
|
||||||
|
|
||||||
const iconMap = {
|
export function DiffStatText({ diff }: { diff: Required<TreeNode>['diff'] }) {
|
||||||
added: 'diffAdded',
|
const { additions, deletions } = diff
|
||||||
ignored: 'diffIgnored',
|
|
||||||
modified: 'diffModified',
|
|
||||||
removed: 'diffRemoved',
|
|
||||||
renamed: 'diffRenamed',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiffStatText({
|
|
||||||
diff: { status, additions, deletions },
|
|
||||||
}: {
|
|
||||||
diff: Required<TreeNode>['diff']
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<span className={'diff-stat-text'}>
|
<span className={'diff-stat-text'}>
|
||||||
<Icon className={status} type={iconMap[status]} />
|
|
||||||
{additions > 0 && <span className={'additions'}>{additions}</span>}
|
{additions > 0 && <span className={'additions'}>{additions}</span>}
|
||||||
{additions > 0 && deletions > 0 && '/'}
|
{additions > 0 && deletions > 0 && '/'}
|
||||||
{deletions > 0 && <span className={'deletions'}>{deletions}</span>}
|
{deletions > 0 && <span className={'deletions'}>{deletions}</span>}
|
||||||
|
|
|
||||||
37
src/components/FileExplorer/hooks/useHandleClickFileLink.ts
Normal file
37
src/components/FileExplorer/hooks/useHandleClickFileLink.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { useEvent } from 'react-use'
|
||||||
|
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
|
||||||
|
|
||||||
|
export function useHandleClickFileLink(ref: React.MutableRefObject<HTMLElement | null>) {
|
||||||
|
const toGoRef = useRef<string | null>(null)
|
||||||
|
const onClickCaptureSaveHrefWithHash = useCallback(
|
||||||
|
(e: Event) => {
|
||||||
|
const target = e.target
|
||||||
|
if (target instanceof HTMLElement && ref.current?.contains(target)) {
|
||||||
|
let e: HTMLElement | null = target
|
||||||
|
while (e && e !== ref.current) {
|
||||||
|
if (e instanceof HTMLAnchorElement) {
|
||||||
|
const hash = e.href.split('#')[1]
|
||||||
|
if (hash) {
|
||||||
|
toGoRef.current = e.href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e = e.parentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ref],
|
||||||
|
)
|
||||||
|
useEvent('click', onClickCaptureSaveHrefWithHash, document, true)
|
||||||
|
|
||||||
|
const redirectToSavedHrefWithHash = useCallback(() => {
|
||||||
|
const toGo = toGoRef.current
|
||||||
|
if (toGo) {
|
||||||
|
toGoRef.current = null
|
||||||
|
if (toGo.startsWith(location.href)) {
|
||||||
|
window.location.replace(toGo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
useAfterRedirect(redirectToSavedHrefWithHash)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
CheckCircleFillIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CommentIcon,
|
CommentIcon,
|
||||||
CrossReferenceIcon,
|
CrossReferenceIcon,
|
||||||
|
|
@ -15,6 +17,7 @@ import { cancelEvent, onEnterKeyDown } from 'utils/DOMHelper'
|
||||||
import { is } from 'utils/is'
|
import { is } from 'utils/is'
|
||||||
import { Icon } from '../../Icon'
|
import { Icon } from '../../Icon'
|
||||||
import { SearchMode } from '../../searchModes'
|
import { SearchMode } from '../../searchModes'
|
||||||
|
import { DiffIcon } from '../DiffIcon'
|
||||||
import { DiffStatText } from '../DiffStatText'
|
import { DiffStatText } from '../DiffStatText'
|
||||||
import { DiffStatGraph } from './../DiffStatGraph'
|
import { DiffStatGraph } from './../DiffStatGraph'
|
||||||
import { VisibleNodesGeneratorMethods } from './useVisibleNodesGeneratorMethods'
|
import { VisibleNodesGeneratorMethods } from './useVisibleNodesGeneratorMethods'
|
||||||
|
|
@ -34,16 +37,27 @@ export function useNodeRenderers(allRenderers: (NodeRenderer | null | undefined)
|
||||||
export function useRenderFileStatus() {
|
export function useRenderFileStatus() {
|
||||||
const { showDiffInText } = useConfigs().value
|
const { showDiffInText } = useConfigs().value
|
||||||
return useCallback(
|
return useCallback(
|
||||||
function renderFileStatus({ diff }: TreeNode) {
|
function renderFileStatus({ diff, reviewed }: TreeNode) {
|
||||||
return (
|
return (
|
||||||
diff && (
|
<>
|
||||||
<span
|
{diff && (
|
||||||
className={'node-item-diff'}
|
<span
|
||||||
title={`${diff.status}, ${diff.changes} changes: +${diff.additions} & -${diff.deletions}`}
|
className={'node-item-diff'}
|
||||||
>
|
title={`${diff.status}, ${diff.changes} changes: +${diff.additions} & -${diff.deletions}`}
|
||||||
{showDiffInText ? <DiffStatText diff={diff} /> : <DiffStatGraph diff={diff} />}
|
>
|
||||||
</span>
|
<DiffIcon diff={diff} />
|
||||||
)
|
{showDiffInText ? <DiffStatText diff={diff} /> : <DiffStatGraph diff={diff} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{reviewed === undefined ? null : (
|
||||||
|
<span
|
||||||
|
className={cx('node-item-reviewed', { reviewed })}
|
||||||
|
title={reviewed ? 'Reviewed' : 'Not reviewed'}
|
||||||
|
>
|
||||||
|
<Icon IconComponent={reviewed ? CheckCircleFillIcon : CheckCircleIcon} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[showDiffInText],
|
[showDiffInText],
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ export function useHandleNodeClick(
|
||||||
|
|
||||||
focusNode(node)
|
focusNode(node)
|
||||||
if (node.url) {
|
if (node.url) {
|
||||||
const isHashLink = node.url.includes('#')
|
const isHashLink =
|
||||||
|
node.url.includes('#') && node.url.split('#')[0] === location.pathname
|
||||||
if (!isHashLink) {
|
if (!isHashLink) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
loadWithFastRedirect(node.url, event.currentTarget)
|
loadWithFastRedirect(node.url, event.currentTarget)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useConfigs } from 'containers/ConfigsContext'
|
||||||
import { useInspector } from 'containers/Inspector'
|
import { useInspector } from 'containers/Inspector'
|
||||||
import { PortalContext } from 'containers/PortalContext'
|
import { PortalContext } from 'containers/PortalContext'
|
||||||
import { RepoContext } from 'containers/RepoContext'
|
import { RepoContext } from 'containers/RepoContext'
|
||||||
|
import { platform } from 'platforms'
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
|
@ -25,6 +26,7 @@ import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
|
||||||
import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
|
import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
|
||||||
import { SideBarStateContext } from '../../containers/SideBarState'
|
import { SideBarStateContext } from '../../containers/SideBarState'
|
||||||
import { useGetCurrentPath } from './hooks/useGetCurrentPath'
|
import { useGetCurrentPath } from './hooks/useGetCurrentPath'
|
||||||
|
import { useHandleClickFileLink } from './hooks/useHandleClickFileLink'
|
||||||
import { useHandleKeyDown } from './hooks/useHandleKeyDown'
|
import { useHandleKeyDown } from './hooks/useHandleKeyDown'
|
||||||
import {
|
import {
|
||||||
NodeRenderer,
|
NodeRenderer,
|
||||||
|
|
@ -59,6 +61,8 @@ export function FileExplorer() {
|
||||||
const visibleNodes = useVisibleNodes(visibleNodesGenerator)
|
const visibleNodes = useVisibleNodes(visibleNodesGenerator)
|
||||||
const state = useLoadedContext(SideBarStateContext).value
|
const state = useLoadedContext(SideBarStateContext).value
|
||||||
|
|
||||||
|
platform.usePlatformFileTreeHooks?.({ visibleNodesGenerator })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{run(() => {
|
{run(() => {
|
||||||
|
|
@ -196,6 +200,8 @@ function LoadedFileExplorer({
|
||||||
useCallback(() => ref.current?.focus(), []),
|
useCallback(() => ref.current?.focus(), []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useHandleClickFileLink(ref)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={`file-explorer`} tabIndex={-1} onKeyDown={handleKeyDown}>
|
<div ref={ref} className={`file-explorer`} tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
{visibleNodesGenerator?.defer && (
|
{visibleNodesGenerator?.defer && (
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
|
||||||
({
|
({
|
||||||
regex: isValidRegexpSource(value),
|
regex: isValidRegexpSource(value),
|
||||||
fuzzy: true,
|
fuzzy: true,
|
||||||
}[searchMode]),
|
})[searchMode],
|
||||||
[value, searchMode],
|
[value, searchMode],
|
||||||
)
|
)
|
||||||
const isSupportedRegex = useMemo(
|
const isSupportedRegex = useMemo(
|
||||||
|
|
@ -45,8 +45,8 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
|
||||||
? !isInputValid
|
? !isInputValid
|
||||||
? 'Invalid regular expression.'
|
? 'Invalid regular expression.'
|
||||||
: !isSupportedRegex
|
: !isSupportedRegex
|
||||||
? `Highlight is not supported for regular expression containing '?:', '?=', '?!', '?<=', or '?<!.'`
|
? `Highlight is not supported for regular expression containing '?:', '?=', '?!', '?<=', or '?<!.'`
|
||||||
: null
|
: null
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const [focused, setFocused] = React.useState(false)
|
const [focused, setFocused] = React.useState(false)
|
||||||
|
|
|
||||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
|
|
@ -19,6 +19,7 @@ type TreeNode = {
|
||||||
rawLink?: string
|
rawLink?: string
|
||||||
sha?: string
|
sha?: string
|
||||||
accessDenied?: boolean
|
accessDenied?: boolean
|
||||||
|
reviewed?: boolean
|
||||||
comments?: {
|
comments?: {
|
||||||
active: number
|
active: number
|
||||||
resolved: number
|
resolved: number
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import { errors } from 'platforms'
|
||||||
import { isEnterprise } from '.'
|
import { isEnterprise } from '.'
|
||||||
import { is } from '../../utils/is'
|
import { is } from '../../utils/is'
|
||||||
import { gitakoServiceHost } from '../../utils/networkService'
|
import { gitakoServiceHost } from '../../utils/networkService'
|
||||||
import { continuousLoadFragmentedPages, getDOM, resolveHeaderLink } from './utils'
|
import {
|
||||||
|
continuousLoadFragmentedPages,
|
||||||
|
continuousLoadFragmentedPagesFromUrl,
|
||||||
|
resolveHeaderLink,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
function isAPIRateLimitExceeded(content: JSONValue) {
|
function isAPIRateLimitExceeded(content: JSONValue) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -144,21 +148,24 @@ export async function getPullPageDocuments(
|
||||||
userName: string,
|
userName: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
pullId: string,
|
pullId: string,
|
||||||
document?: Document,
|
preset?: {
|
||||||
): Promise<Document[]> {
|
url: string
|
||||||
// Response of this API contains view of few files but is not complete.
|
document: Document
|
||||||
return continuousLoadFragmentedPages(
|
},
|
||||||
document ||
|
) {
|
||||||
(await getDOM(`${window.location.origin}/${userName}/${repoName}/pull/${pullId}/files`)),
|
if (preset) {
|
||||||
)
|
return continuousLoadFragmentedPages(preset.url, preset.document)
|
||||||
|
}
|
||||||
|
// Response of this contains view of few files but is not complete.
|
||||||
|
return continuousLoadFragmentedPagesFromUrl(`/${userName}/${repoName}/pull/${pullId}/files`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCommitPageDocuments(): Promise<Document[]> {
|
export async function getCommitPageDocuments() {
|
||||||
/* userName: string,
|
/* userName: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
commitId: string, */
|
commitId: string, */
|
||||||
// arguments are not used because info are collected from DOM directly
|
// arguments are not used because info are collected from DOM directly
|
||||||
return continuousLoadFragmentedPages(document)
|
return continuousLoadFragmentedPages(window.location.href, document)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBlobData(
|
export async function getBlobData(
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ import { raiseError } from 'analytics'
|
||||||
import { Clippy, ClippyClassName } from 'components/Clippy'
|
import { Clippy, ClippyClassName } from 'components/Clippy'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as s from 'superstruct'
|
import * as s from 'superstruct'
|
||||||
import { $ } from 'utils/$'
|
import { $, make$ } from 'utils/$'
|
||||||
import { formatClass, parseIntFromElement } from 'utils/DOMHelper'
|
import { formatClass, parseIntFromElement } from 'utils/DOMHelper'
|
||||||
import { renderReact } from 'utils/general'
|
import { renderReact } from 'utils/general'
|
||||||
import { embeddedDataStruct } from './embeddedDataStructures'
|
import { embeddedDataStruct } from './embeddedDataStructures'
|
||||||
|
|
||||||
const selectors = {
|
const selectors = {
|
||||||
normal: {
|
normal: {
|
||||||
|
userName: '[itemprop="author"] > a[rel="author"]',
|
||||||
|
repoName: '[itemprop="name"] > a[href]',
|
||||||
reactApp: `react-app[app-name="react-code-view"] [data-target="react-app.reactRoot"]`,
|
reactApp: `react-app[app-name="react-code-view"] [data-target="react-app.reactRoot"]`,
|
||||||
codeTab: '#code-tab',
|
codeTab: '#code-tab',
|
||||||
branchSwitcher: [
|
branchSwitcher: [
|
||||||
|
|
@ -24,30 +26,40 @@ const selectors = {
|
||||||
globalNavigation: {
|
globalNavigation: {
|
||||||
navbar: {
|
navbar: {
|
||||||
repositoryOwner: [
|
repositoryOwner: [
|
||||||
|
'nav[role="navigation"][aria-label="GitHub Breadcrumb"] [id^="contextregion-usercrumb"][id$="-link"]',
|
||||||
'.AppHeader-context-item[data-hovercard-type="user"]',
|
'.AppHeader-context-item[data-hovercard-type="user"]',
|
||||||
'.AppHeader-context-item[data-hovercard-type="organization"]',
|
'.AppHeader-context-item[data-hovercard-type="organization"]',
|
||||||
].join(),
|
].join(),
|
||||||
// its meant to be the element visually next to the `repositoryOwner` element
|
// its meant to be the element visually next to the `repositoryOwner` element
|
||||||
repositoryName:
|
repositoryName: [
|
||||||
|
'nav[role="navigation"][aria-label="GitHub Breadcrumb"] [id^="contextregion-repositorycrumb"][id$="-link"]',
|
||||||
'nav[role="navigation"] ul[role="list"] li:nth-child(2) .AppHeader-context-item',
|
'nav[role="navigation"] ul[role="list"] li:nth-child(2) .AppHeader-context-item',
|
||||||
|
].join(),
|
||||||
},
|
},
|
||||||
branchSelector: 'button[id^="branch-picker-"]',
|
treeViewBranchSelector: ['#react-repos-tree-pane-ref-selector'].join(),
|
||||||
|
branchSelector: [
|
||||||
|
'button[id^="branch-picker-"]',
|
||||||
|
'#ref-picker-repos-header-ref-selector-wide',
|
||||||
|
].join(),
|
||||||
pathContext: '[data-testid="breadcrumbs"]',
|
pathContext: '[data-testid="breadcrumbs"]',
|
||||||
pathContextFileName: '[data-testid="breadcrumbs-filename"]',
|
pathContextFileName: '[data-testid="breadcrumbs-filename"]',
|
||||||
pathContextScreenReaderHeading: '[data-testid="screen-reader-heading"]',
|
pathContextScreenReaderHeading: '[data-testid="screen-reader-heading"]',
|
||||||
embeddedData: {
|
embeddedData: {
|
||||||
app: 'script[type="application/json"][data-target="react-app.embeddedData"]',
|
app: 'script[type="application/json"][data-target="react-app.embeddedData"]',
|
||||||
|
reactAppCodeView:
|
||||||
|
'react-app[app-name="react-code-view"] script[type="application/json"][data-target="react-app.embeddedData"]',
|
||||||
reposOverview:
|
reposOverview:
|
||||||
'[partial-name="repos-overview"] script[type="application/json"][data-target="react-partial.embeddedData"]',
|
'[partial-name="repos-overview"] script[type="application/json"][data-target="react-partial.embeddedData"]',
|
||||||
|
pullRequest: 'script[type="application/json"][data-target="react-app.embeddedData"]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDOMJSON = (selector: string) =>
|
const getDOMJSON = (selector: string, _$ = $) =>
|
||||||
$(selector, e => {
|
_$(selector, e => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(e.textContent || '')
|
return JSON.parse(e.textContent || '')
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -73,13 +85,23 @@ function resolveEmbeddedAppData() {
|
||||||
if (s.is(data, embeddedDataStruct.app)) return getMetaFromPayload(data.payload)
|
if (s.is(data, embeddedDataStruct.app)) return getMetaFromPayload(data.payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveEmbeddedCodeViewData() {
|
||||||
|
const data = getDOMJSON(selectors.globalNavigation.embeddedData.reactAppCodeView)
|
||||||
|
if (s.is(data, embeddedDataStruct.codeViewApp)) return data.payload
|
||||||
|
}
|
||||||
|
|
||||||
function resolveEmbeddedReposOverviewData() {
|
function resolveEmbeddedReposOverviewData() {
|
||||||
const data = getDOMJSON(selectors.globalNavigation.embeddedData.reposOverview)
|
const data = getDOMJSON(selectors.globalNavigation.embeddedData.reposOverview)
|
||||||
if (s.is(data, embeddedDataStruct.reposOverview))
|
if (s.is(data, embeddedDataStruct.reposOverview))
|
||||||
return getMetaFromPayload(data.props.initialPayload)
|
return getMetaFromPayload(data.props.initialPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveEmbeddedData(): {
|
export function resolveEmbeddedPullRequestData(doc: Document) {
|
||||||
|
const data = getDOMJSON(selectors.globalNavigation.embeddedData.pullRequest, make$(doc))
|
||||||
|
if (s.is(data, embeddedDataStruct.pullRequest)) return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMetaFromEmbeddedData(): {
|
||||||
defaultBranch: string
|
defaultBranch: string
|
||||||
metaData: MetaData
|
metaData: MetaData
|
||||||
} | void {
|
} | void {
|
||||||
|
|
@ -87,19 +109,19 @@ export function resolveEmbeddedData(): {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMeta(): Partial<MetaData> {
|
export function resolveMeta(): Partial<MetaData> {
|
||||||
const dataFromJSON = resolveEmbeddedData()
|
const dataFromJSON = resolveMetaFromEmbeddedData()
|
||||||
if (dataFromJSON) return dataFromJSON.metaData
|
if (dataFromJSON) return dataFromJSON.metaData
|
||||||
|
|
||||||
const metaData = {
|
const metaData = {
|
||||||
userName:
|
userName:
|
||||||
$(
|
$(
|
||||||
'[itemprop="author"] > a[rel="author"]',
|
[selectors.normal.userName, selectors.globalNavigation.navbar.repositoryOwner].join(),
|
||||||
e => e.textContent?.trim(),
|
e => e.textContent?.trim(),
|
||||||
() => $(selectors.globalNavigation.navbar.repositoryOwner, e => e.textContent?.trim()),
|
() => $(selectors.globalNavigation.navbar.repositoryOwner, e => e.textContent?.trim()),
|
||||||
) || undefined,
|
) || undefined,
|
||||||
repoName:
|
repoName:
|
||||||
$(
|
$(
|
||||||
'[itemprop="name"] > a[href]',
|
[selectors.normal.repoName, selectors.globalNavigation.navbar.repositoryName].join(),
|
||||||
e => e.textContent?.trim(),
|
e => e.textContent?.trim(),
|
||||||
() => $(selectors.globalNavigation.navbar.repositoryName, e => e.textContent?.trim()),
|
() => $(selectors.globalNavigation.navbar.repositoryName, e => e.textContent?.trim()),
|
||||||
) || undefined,
|
) || undefined,
|
||||||
|
|
@ -114,6 +136,12 @@ export function resolveMeta(): Partial<MetaData> {
|
||||||
export function isInRepoPage() {
|
export function isInRepoPage() {
|
||||||
const repoHeadSelector = '.repohead'
|
const repoHeadSelector = '.repohead'
|
||||||
const authorNameSelector = '.author[itemprop="author"]'
|
const authorNameSelector = '.author[itemprop="author"]'
|
||||||
|
const repoMetaSelector = [
|
||||||
|
'meta[name="octolytics-dimension-repository_nwo"]',
|
||||||
|
'meta[name="octolytics-dimension-repository_id"]',
|
||||||
|
].join()
|
||||||
|
if (document.querySelector(repoMetaSelector)) return true
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
document.querySelector(
|
document.querySelector(
|
||||||
[
|
[
|
||||||
|
|
@ -139,7 +167,16 @@ export function isInPullFilesPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIssueTitle() {
|
export function getIssueTitle() {
|
||||||
const title = $('.gh-header-title')?.textContent
|
const titleContainerSelectors = [
|
||||||
|
'[data-component="TitleArea"] [data-component="PH_Title"]', // PR new experience title
|
||||||
|
'.gh-header-title',
|
||||||
|
]
|
||||||
|
const title = (
|
||||||
|
$(
|
||||||
|
// exclude issue ID from title
|
||||||
|
titleContainerSelectors.map(selector => `${selector} .markdown-title`).join(),
|
||||||
|
) ?? $(titleContainerSelectors.join())
|
||||||
|
)?.textContent
|
||||||
return title?.trim().replace(/\n/g, '')
|
return title?.trim().replace(/\n/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +186,22 @@ export function getCommitTitle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentBranch(passive = false) {
|
export function getCurrentBranch(passive = false) {
|
||||||
|
const embeddedData = resolveEmbeddedCodeViewData()
|
||||||
|
if (embeddedData) {
|
||||||
|
return embeddedData.refInfo.name
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const treeViewSelectedBranchButtonSelector = [
|
||||||
|
selectors.globalNavigation.treeViewBranchSelector,
|
||||||
|
].join()
|
||||||
|
const treeViewSelectedBranchButtonElement = $(treeViewSelectedBranchButtonSelector)
|
||||||
|
const branchName = treeViewSelectedBranchButtonElement?.textContent.trim()
|
||||||
|
if (branchName) {
|
||||||
|
return branchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedBranchButtonSelector = [
|
const selectedBranchButtonSelector = [
|
||||||
'main #branch-select-menu summary',
|
'main #branch-select-menu summary',
|
||||||
'main .branch-select-menu summary',
|
'main .branch-select-menu summary',
|
||||||
|
|
@ -194,7 +247,9 @@ export function getCurrentBranch(passive = false) {
|
||||||
})
|
})
|
||||||
if (branchNameFromCodeTab) return branchNameFromCodeTab
|
if (branchNameFromCodeTab) return branchNameFromCodeTab
|
||||||
|
|
||||||
if (!passive) raiseError(new Error('cannot get current branch'))
|
if (!passive) {
|
||||||
|
raiseError(new Error('cannot get current branch'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as s from 'superstruct'
|
import * as s from 'superstruct'
|
||||||
|
|
||||||
const repo = s.object({
|
const repo = s.type({
|
||||||
id: s.number(),
|
id: s.number(),
|
||||||
defaultBranch: s.string(),
|
defaultBranch: s.string(),
|
||||||
name: s.string(),
|
name: s.string(),
|
||||||
|
|
@ -15,13 +15,13 @@ const repo = s.object({
|
||||||
isOrgOwned: s.boolean(),
|
isOrgOwned: s.boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = s.object({
|
const user = s.type({
|
||||||
id: s.number(),
|
id: s.number(),
|
||||||
login: s.string(),
|
login: s.string(),
|
||||||
userEmail: s.string(),
|
userEmail: s.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const rel = s.object({
|
const rel = s.type({
|
||||||
name: s.string(),
|
name: s.string(),
|
||||||
listCacheKey: s.string(),
|
listCacheKey: s.string(),
|
||||||
canEdit: s.boolean(),
|
canEdit: s.boolean(),
|
||||||
|
|
@ -29,13 +29,13 @@ const rel = s.object({
|
||||||
currentOid: s.string(),
|
currentOid: s.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const treeItem = s.object({
|
const treeItem = s.type({
|
||||||
name: s.string(),
|
name: s.string(),
|
||||||
path: s.string(),
|
path: s.string(),
|
||||||
contentType: s.string(),
|
contentType: s.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const tree = s.object({
|
const tree = s.type({
|
||||||
items: s.array(treeItem),
|
items: s.array(treeItem),
|
||||||
templateDirectorySuggestionUrl: s.nullable(s.never()),
|
templateDirectorySuggestionUrl: s.nullable(s.never()),
|
||||||
readme: s.nullable(s.never()),
|
readme: s.nullable(s.never()),
|
||||||
|
|
@ -43,7 +43,7 @@ const tree = s.object({
|
||||||
showBranchInfobar: s.boolean(),
|
showBranchInfobar: s.boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const repoPayload = s.object({
|
const repoPayload = s.type({
|
||||||
allShortcutsEnabled: s.boolean(),
|
allShortcutsEnabled: s.boolean(),
|
||||||
path: s.string(),
|
path: s.string(),
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
|
@ -59,16 +59,58 @@ const repoPayload = s.object({
|
||||||
overview: s.unknown(),
|
overview: s.unknown(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const reposOverview = s.object({
|
const reposOverview = s.type({
|
||||||
props: s.object({
|
props: s.type({
|
||||||
initialPayload: repoPayload,
|
initialPayload: repoPayload,
|
||||||
appPayload: s.unknown(),
|
appPayload: s.unknown(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const app = s.object({
|
|
||||||
|
const app = s.type({
|
||||||
payload: repoPayload,
|
payload: repoPayload,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const codeViewApp = s.type({
|
||||||
|
payload: s.type({
|
||||||
|
refInfo: s.type({
|
||||||
|
name: s.string(),
|
||||||
|
refType: s.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const diffSummary = s.type({
|
||||||
|
changeType: s.string(),
|
||||||
|
highestAnnotationLevel: s.nullable(s.string()),
|
||||||
|
isCodeowner: s.nullable(s.boolean()),
|
||||||
|
isManifestFile: s.boolean(),
|
||||||
|
isSymlink: s.boolean(),
|
||||||
|
isVendored: s.boolean(),
|
||||||
|
linesAdded: s.number(),
|
||||||
|
linesChanged: s.number(),
|
||||||
|
linesDeleted: s.number(),
|
||||||
|
markedAsViewed: s.boolean(),
|
||||||
|
path: s.string(),
|
||||||
|
pathDigest: s.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type DiffSummary = s.Infer<typeof diffSummary>
|
||||||
|
|
||||||
|
const pullRequest = s.type({
|
||||||
|
payload: s.type({
|
||||||
|
pullRequestsFilesRoute: s.optional(
|
||||||
|
s.type({
|
||||||
|
diffSummaries: s.array(diffSummary),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
pullRequestsChangesRoute: s.optional(
|
||||||
|
s.type({
|
||||||
|
diffSummaries: s.array(diffSummary),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
export const embeddedDataStruct = {
|
export const embeddedDataStruct = {
|
||||||
repo,
|
repo,
|
||||||
user,
|
user,
|
||||||
|
|
@ -78,4 +120,6 @@ export const embeddedDataStruct = {
|
||||||
repoPayload,
|
repoPayload,
|
||||||
reposOverview,
|
reposOverview,
|
||||||
app,
|
app,
|
||||||
|
codeViewApp,
|
||||||
|
pullRequest,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export async function getCommitTreeData(
|
||||||
.map(({ files }) => files)
|
.map(({ files }) => files)
|
||||||
.flat()
|
.flat()
|
||||||
|
|
||||||
const documents = await API.getCommitPageDocuments(/* userName, repoName, commitSHA */)
|
const [, documents] = await API.getCommitPageDocuments(/* userName, repoName, commitSHA */)
|
||||||
|
|
||||||
const getItemURL = (path: string) => {
|
const getItemURL = (path: string) => {
|
||||||
for (const doc of documents) {
|
for (const doc of documents) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
import { map } from 'utils/map'
|
||||||
import { sanitizedLocation } from 'utils/URLHelper'
|
import { sanitizedLocation } from 'utils/URLHelper'
|
||||||
import * as API from './API'
|
import * as API from './API'
|
||||||
import { getPRDiffTotalStat, getPullRequestFilesCount, isInPullFilesPage } from './DOMHelper'
|
import {
|
||||||
|
getPRDiffTotalStat,
|
||||||
|
getPullRequestFilesCount,
|
||||||
|
isInPullFilesPage,
|
||||||
|
resolveEmbeddedPullRequestData,
|
||||||
|
} from './DOMHelper'
|
||||||
|
import { DiffSummary } from './embeddedDataStructures'
|
||||||
import { processTree } from './index'
|
import { processTree } from './index'
|
||||||
import { getCommentsMap } from './utils'
|
import { getCommentsMap } from './utils'
|
||||||
|
|
||||||
|
|
@ -31,28 +38,27 @@ export async function getPullRequestTreeData(
|
||||||
API.getPullComments(userName, repoName, pullId, accessToken),
|
API.getPullComments(userName, repoName, pullId, accessToken),
|
||||||
])
|
])
|
||||||
|
|
||||||
const docs = await API.getPullPageDocuments(
|
const [fileChangesPagePath, docs] = await API.getPullPageDocuments(
|
||||||
userName,
|
userName,
|
||||||
repoName,
|
repoName,
|
||||||
pullId,
|
pullId,
|
||||||
isInPullFilesPage() ? document : undefined,
|
isInPullFilesPage()
|
||||||
|
? {
|
||||||
|
url: window.location.href,
|
||||||
|
document,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
)
|
)
|
||||||
// query all elements at once to make getFileElementHash run faster
|
|
||||||
const elementsHavePath = docs.map(doc => doc.querySelectorAll(`[data-path]`))
|
const diffSummaryMap = resolveDiffSummaryMap(docs)
|
||||||
const map = new Map<string, string>()
|
|
||||||
for (const group of elementsHavePath) {
|
const fileHashMap =
|
||||||
for (let i = 0; i < group.length; i++) {
|
diffSummaryMap.size > 0
|
||||||
const element = group[i]
|
? new Map(map(diffSummaryMap, ([, { path, pathDigest }]) => [path, `diff-${pathDigest}`]))
|
||||||
const id = element.parentElement?.id
|
: resolveFileHashMap(docs)
|
||||||
if (id) {
|
|
||||||
const path = element.getAttribute('data-path')
|
|
||||||
if (path) map.set(path, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(sanitizedLocation.href)
|
const url = new URL(sanitizedLocation.href)
|
||||||
url.pathname = `/${userName}/${repoName}/pull/${pullId}/files`
|
url.pathname = fileChangesPagePath
|
||||||
const commentsMap = getCommentsMap(commentData)
|
const commentsMap = getCommentsMap(commentData)
|
||||||
const nodes: TreeNode[] = treeData.map(
|
const nodes: TreeNode[] = treeData.map(
|
||||||
({
|
({
|
||||||
|
|
@ -65,7 +71,7 @@ export async function getPullRequestTreeData(
|
||||||
raw_url: rawLink,
|
raw_url: rawLink,
|
||||||
blob_url: permalink,
|
blob_url: permalink,
|
||||||
}) => {
|
}) => {
|
||||||
url.hash = map.get(filename) || ''
|
url.hash = fileHashMap.get(filename) || ''
|
||||||
return {
|
return {
|
||||||
path: filename || '',
|
path: filename || '',
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
|
|
@ -74,6 +80,7 @@ export async function getPullRequestTreeData(
|
||||||
permalink,
|
permalink,
|
||||||
rawLink,
|
rawLink,
|
||||||
sha,
|
sha,
|
||||||
|
reviewed: diffSummaryMap.get(filename)?.markedAsViewed,
|
||||||
comments: commentsMap.get(filename),
|
comments: commentsMap.get(filename),
|
||||||
diff: {
|
diff: {
|
||||||
status,
|
status,
|
||||||
|
|
@ -93,6 +100,45 @@ const GITHUB_API_RESPONSE_LENGTH_LIMIT = 3000
|
||||||
const GITHUB_API_RESPONSE_MAX_SIZE_PER_PAGE = 100
|
const GITHUB_API_RESPONSE_MAX_SIZE_PER_PAGE = 100
|
||||||
const MAX_PAGE = Math.ceil(GITHUB_API_RESPONSE_LENGTH_LIMIT / GITHUB_API_RESPONSE_MAX_SIZE_PER_PAGE)
|
const MAX_PAGE = Math.ceil(GITHUB_API_RESPONSE_LENGTH_LIMIT / GITHUB_API_RESPONSE_MAX_SIZE_PER_PAGE)
|
||||||
|
|
||||||
|
function resolveFileHashMap(docs: Document[]) {
|
||||||
|
// query all elements at once to make getFileElementHash run faster
|
||||||
|
const elementsHavePath = docs.map(doc => doc.querySelectorAll(`[data-path]`))
|
||||||
|
const fileHashMap = new Map<string, string>()
|
||||||
|
for (const group of elementsHavePath) {
|
||||||
|
for (let i = 0; i < group.length; i++) {
|
||||||
|
const element = group[i]
|
||||||
|
const id = element.parentElement?.id
|
||||||
|
if (id) {
|
||||||
|
const path = element.getAttribute('data-path')
|
||||||
|
if (path) fileHashMap.set(path, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileHashMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDiffSummaryMap(docs: Document[]) {
|
||||||
|
return docs
|
||||||
|
.map(resolveEmbeddedPullRequestData)
|
||||||
|
.map(json => {
|
||||||
|
const payload = json?.payload
|
||||||
|
if (!payload) return null
|
||||||
|
if ('pullRequestsFilesRoute' in payload) {
|
||||||
|
return payload.pullRequestsFilesRoute
|
||||||
|
} else if ('pullRequestsChangesRoute' in payload) {
|
||||||
|
return payload.pullRequestsChangesRoute
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(pullRequests => pullRequests?.diffSummaries)
|
||||||
|
.reduce((map, curr) => {
|
||||||
|
curr?.forEach(record => {
|
||||||
|
map.set(record.path, record)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, new Map<DiffSummary['path'], DiffSummary>())
|
||||||
|
}
|
||||||
|
|
||||||
async function safeGetPullRequestTreeData(
|
async function safeGetPullRequestTreeData(
|
||||||
{ userName, repoName }: Pick<MetaData, 'userName' | 'repoName' | 'branchName'>,
|
{ userName, repoName }: Pick<MetaData, 'userName' | 'repoName' | 'branchName'>,
|
||||||
pullId: string,
|
pullId: string,
|
||||||
|
|
|
||||||
30
src/platforms/GitHub/hooks/useGitHubReviewStatus.ts
Normal file
30
src/platforms/GitHub/hooks/useGitHubReviewStatus.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
|
||||||
|
|
||||||
|
export function useGitHubReviewStatus(visibleNodesGenerator: VisibleNodesGenerator | null) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!visibleNodesGenerator) return
|
||||||
|
|
||||||
|
const clickHandler = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
const markReviewedButton = target.closest('[class*="MarkAsViewedButton-"]')
|
||||||
|
const filePath = markReviewedButton
|
||||||
|
?.closest('[id^="diff-"]')
|
||||||
|
?.querySelector('a[href^="#diff-"]')
|
||||||
|
?.textContent?.trim()
|
||||||
|
// remove Unicode control characters that GitHub adds for RTL support
|
||||||
|
.replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '')
|
||||||
|
|
||||||
|
if (!markReviewedButton) return
|
||||||
|
if (!filePath) return
|
||||||
|
visibleNodesGenerator.updateNode(filePath, node => {
|
||||||
|
node.reviewed = markReviewedButton.getAttribute('aria-label') === 'Viewed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('click', clickHandler)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', clickHandler)
|
||||||
|
}
|
||||||
|
}, [visibleNodesGenerator])
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useConfigs } from 'containers/ConfigsContext'
|
import { useConfigs } from 'containers/ConfigsContext'
|
||||||
import { GITHUB_OAUTH } from 'env'
|
import { GITHUB_OAUTH } from 'env'
|
||||||
import { Base64 } from 'js-base64'
|
import { Base64 } from 'js-base64'
|
||||||
|
import { Platform } from 'platforms/platform'
|
||||||
import { $ } from 'utils/$'
|
import { $ } from 'utils/$'
|
||||||
import { configRef } from 'utils/config/helper'
|
import { configRef } from 'utils/config/helper'
|
||||||
import { resolveGitModules } from 'utils/gitSubmodule'
|
import { resolveGitModules } from 'utils/gitSubmodule'
|
||||||
|
|
@ -13,6 +14,7 @@ import { getPullRequestTreeData } from './getPullRequestTreeData'
|
||||||
import { useEnterpriseStatBarStyleFix } from './hooks/useEnterpriseStatBarStyleFix'
|
import { useEnterpriseStatBarStyleFix } from './hooks/useEnterpriseStatBarStyleFix'
|
||||||
import { useGitHubAttachCopySnippetButton } from './hooks/useGitHubAttachCopySnippetButton'
|
import { useGitHubAttachCopySnippetButton } from './hooks/useGitHubAttachCopySnippetButton'
|
||||||
import { useGitHubCodeFold } from './hooks/useGitHubCodeFold'
|
import { useGitHubCodeFold } from './hooks/useGitHubCodeFold'
|
||||||
|
import { useGitHubReviewStatus } from './hooks/useGitHubReviewStatus'
|
||||||
|
|
||||||
export function processTree(tree: TreeNode[]): TreeNode {
|
export function processTree(tree: TreeNode[]): TreeNode {
|
||||||
// nodes are created from items and put onto tree
|
// nodes are created from items and put onto tree
|
||||||
|
|
@ -137,7 +139,7 @@ export const GitHub: Platform = {
|
||||||
return metaData
|
return metaData
|
||||||
},
|
},
|
||||||
async getDefaultBranchName({ userName, repoName }, accessToken) {
|
async getDefaultBranchName({ userName, repoName }, accessToken) {
|
||||||
const dataFromJSON = DOMHelper.resolveEmbeddedData()
|
const dataFromJSON = DOMHelper.resolveMetaFromEmbeddedData()
|
||||||
if (dataFromJSON?.defaultBranch) return dataFromJSON.defaultBranch
|
if (dataFromJSON?.defaultBranch) return dataFromJSON.defaultBranch
|
||||||
|
|
||||||
return (await API.getRepoMeta(userName, repoName, accessToken)).default_branch
|
return (await API.getRepoMeta(userName, repoName, accessToken)).default_branch
|
||||||
|
|
@ -150,8 +152,8 @@ export const GitHub: Platform = {
|
||||||
const branchUrl = pullId
|
const branchUrl = pullId
|
||||||
? `${repoUrl}/pull/${pullId}`
|
? `${repoUrl}/pull/${pullId}`
|
||||||
: commitId && URLHelper.isPossiblyCommitSHA(commitId)
|
: commitId && URLHelper.isPossiblyCommitSHA(commitId)
|
||||||
? `${repoUrl}/tree/${commitId}`
|
? `${repoUrl}/tree/${commitId}`
|
||||||
: `${repoUrl}/tree/${branchName}`
|
: `${repoUrl}/tree/${branchName}`
|
||||||
return {
|
return {
|
||||||
repoUrl,
|
repoUrl,
|
||||||
userUrl,
|
userUrl,
|
||||||
|
|
@ -170,8 +172,8 @@ export const GitHub: Platform = {
|
||||||
shouldExpandSideBar() {
|
shouldExpandSideBar() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
(DOMHelper.isInCodePage() || URLHelper.isInCommitPage() || URLHelper.isInPullPage()) &&
|
(DOMHelper.isInCodePage() || URLHelper.isInCommitPage() || URLHelper.isInPullPage()) &&
|
||||||
!DOMHelper.isNativeFileTreeShown() &&
|
!DOMHelper.isNativeFileTreeShown() &&
|
||||||
!DOMHelper.isNativePRFileTreeShown(),
|
!DOMHelper.isNativePRFileTreeShown(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
shouldExpandAll() {
|
shouldExpandAll() {
|
||||||
|
|
@ -207,6 +209,9 @@ export const GitHub: Platform = {
|
||||||
useGitHubCodeFold(codeFolding)
|
useGitHubCodeFold(codeFolding)
|
||||||
useEnterpriseStatBarStyleFix()
|
useEnterpriseStatBarStyleFix()
|
||||||
},
|
},
|
||||||
|
usePlatformFileTreeHooks({ visibleNodesGenerator = null }) {
|
||||||
|
useGitHubReviewStatus(visibleNodesGenerator)
|
||||||
|
},
|
||||||
delegateFastRedirectAnchorProps() {
|
delegateFastRedirectAnchorProps() {
|
||||||
if (configRef.pjaxMode !== 'native') return
|
if (configRef.pjaxMode !== 'native') return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { findMapFirst } from '../../utils/findMapFirst'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolved from response header `link`
|
* Resolved from response header `link`
|
||||||
*
|
*
|
||||||
|
|
@ -74,11 +76,21 @@ export function resolveHeaderLink(raw: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDOM(url: string) {
|
async function getDOM(url: string) {
|
||||||
return new DOMParser().parseFromString(await (await fetch(url)).text(), 'text/html')
|
const res = await fetch(url)
|
||||||
|
const content = await res.text()
|
||||||
|
return [res.url, new DOMParser().parseFromString(content, 'text/html')] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function continuousLoadFragmentedPages(doc: Document) {
|
export async function continuousLoadFragmentedPages(
|
||||||
|
url: string,
|
||||||
|
doc: Document,
|
||||||
|
docs: Document[] = [],
|
||||||
|
): Promise<[string, Document[]]> {
|
||||||
|
docs.push(doc)
|
||||||
|
|
||||||
|
// const data = resolveEmbeddedPullRequestData(doc)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <include-fragment
|
* <include-fragment
|
||||||
* src="..."
|
* src="..."
|
||||||
|
|
@ -92,22 +104,21 @@ export async function continuousLoadFragmentedPages(doc: Document) {
|
||||||
'.js-diff-progressive-container include-fragment[src]', // legacy support
|
'.js-diff-progressive-container include-fragment[src]', // legacy support
|
||||||
]
|
]
|
||||||
|
|
||||||
const documents: Document[] = [doc]
|
const fragment = findMapFirst(fragmentSelectors, selector => doc.querySelector(selector))
|
||||||
|
if (fragment instanceof HTMLElement) {
|
||||||
const selector = fragmentSelectors.find(selector => doc.querySelector(selector))
|
const src = fragment.getAttribute('src')
|
||||||
if (selector) {
|
if (src) {
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
const fragment = doc.querySelector(selector)
|
|
||||||
if (!(fragment instanceof HTMLElement)) break
|
|
||||||
const src = fragment.getAttribute('src')
|
|
||||||
if (!src) break
|
|
||||||
// Using `src` without origin below would fail in Firefox if the src is an absolute path
|
// Using `src` without origin below would fail in Firefox if the src is an absolute path
|
||||||
doc = await getDOM(new URL(src, window.location.origin).href)
|
// do NOT return here because we need to preserve the first `url` for the final return value
|
||||||
documents.push(doc)
|
await continuousLoadFragmentedPagesFromUrl(src, docs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return documents
|
return [new URL(url).pathname, docs]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function continuousLoadFragmentedPagesFromUrl(url: string, docs: Document[] = []) {
|
||||||
|
const [finalUrl, dom] = await getDOM(new URL(url, window.location.origin).href)
|
||||||
|
return continuousLoadFragmentedPages(finalUrl, dom, docs)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommentsMap(commentData: GitHubAPI.PullComments) {
|
export function getCommentsMap(commentData: GitHubAPI.PullComments) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Base64 } from 'js-base64'
|
import { Base64 } from 'js-base64'
|
||||||
|
import { Platform } from 'platforms/platform'
|
||||||
import { resolveGitModules } from 'utils/gitSubmodule'
|
import { resolveGitModules } from 'utils/gitSubmodule'
|
||||||
import { useProgressBar } from 'utils/hooks/useProgressBar'
|
import { useProgressBar } from 'utils/hooks/useProgressBar'
|
||||||
import { sortFoldersToFront } from 'utils/treeParser'
|
import { sortFoldersToFront } from 'utils/treeParser'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { GITEE_OAUTH } from 'env'
|
import { GITEE_OAUTH } from 'env'
|
||||||
import { Base64 } from 'js-base64'
|
import { Base64 } from 'js-base64'
|
||||||
import { errors, platform } from 'platforms'
|
import { errors, platform } from 'platforms'
|
||||||
|
import { Platform } from 'platforms/platform'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { resolveGitModules } from 'utils/gitSubmodule'
|
import { resolveGitModules } from 'utils/gitSubmodule'
|
||||||
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
|
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
|
||||||
|
|
@ -183,7 +184,7 @@ export const Gitee: Platform = {
|
||||||
mapErrorMessage: (error: Error) =>
|
mapErrorMessage: (error: Error) =>
|
||||||
({
|
({
|
||||||
['Only signed in user is allowed to call APIs.']: errors.BAD_CREDENTIALS,
|
['Only signed in user is allowed to call APIs.']: errors.BAD_CREDENTIALS,
|
||||||
}[error.message]),
|
})[error.message],
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGiteeAttachCopySnippetButton(copySnippetButton: boolean) {
|
export function useGiteeAttachCopySnippetButton(copySnippetButton: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Platform } from './platform'
|
||||||
|
|
||||||
export const dummyPlatformForTypeSafety: Platform = {
|
export const dummyPlatformForTypeSafety: Platform = {
|
||||||
isEnterprise() {
|
isEnterprise() {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
type Platform = {
|
import { VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
|
||||||
|
|
||||||
|
export type Platform = {
|
||||||
shouldActivate?(): boolean
|
shouldActivate?(): boolean
|
||||||
isEnterprise(): boolean
|
isEnterprise(): boolean
|
||||||
// branch name might not be available when resolving from DOM and URL
|
// branch name might not be available when resolving from DOM and URL
|
||||||
|
|
@ -29,5 +31,8 @@ type Platform = {
|
||||||
| void
|
| void
|
||||||
loadWithFastRedirect?(url: string, element: HTMLElement): boolean | void
|
loadWithFastRedirect?(url: string, element: HTMLElement): boolean | void
|
||||||
usePlatformHooks?(): void
|
usePlatformHooks?(): void
|
||||||
|
usePlatformFileTreeHooks?(fileTree: {
|
||||||
|
visibleNodesGenerator?: VisibleNodesGenerator | null
|
||||||
|
}): void
|
||||||
mapErrorMessage?: (error: Error) => string | void
|
mapErrorMessage?: (error: Error) => string | void
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,27 @@
|
||||||
export function $<E extends HTMLElement>(selector: string): E | null
|
export interface $ {
|
||||||
export function $<R1>(selector: string, existCallback: (element: HTMLElement) => R1): R1 | null
|
<E extends HTMLElement>(selector: string): E | null
|
||||||
export function $<R1, R2>(
|
<R1>(selector: string, existCallback: (element: HTMLElement) => R1): R1 | null
|
||||||
selector: string,
|
<R1, R2>(
|
||||||
existCallback: (element: HTMLElement) => R1,
|
selector: string,
|
||||||
otherwise: () => R2,
|
existCallback: (element: HTMLElement) => R1,
|
||||||
): R1 | R2
|
otherwise: () => R2,
|
||||||
export function $<E extends HTMLElement, R2>(
|
): R1 | R2
|
||||||
selector: string,
|
<E extends HTMLElement, R2>(
|
||||||
existCallback: undefined | null,
|
selector: string,
|
||||||
otherwise: () => R2,
|
existCallback: undefined | null,
|
||||||
): E | R2
|
otherwise: () => R2,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
): E | R2
|
||||||
export function $(selector: string, existCallback?: any, otherwise?: any) {
|
|
||||||
const element = document.querySelector(selector)
|
|
||||||
if (element) {
|
|
||||||
return existCallback ? existCallback(element) : element
|
|
||||||
}
|
|
||||||
return otherwise ? otherwise() : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const make$ =
|
||||||
|
(root?: Document | HTMLElement): $ =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(selector: string, existCallback?: any, otherwise?: any) => {
|
||||||
|
const element = (root ?? document).querySelector(selector)
|
||||||
|
if (element) {
|
||||||
|
return existCallback ? existCallback(element) : element
|
||||||
|
}
|
||||||
|
return otherwise ? otherwise() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $: $ = make$()
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { EventHub } from '../EventHub'
|
||||||
import { findNode } from '../general'
|
import { findNode } from '../general'
|
||||||
import { Options } from './index'
|
import { Options } from './index'
|
||||||
|
|
||||||
function mergeNodes(target: TreeNode, source: TreeNode) {
|
function inPlaceMergeNodes(target: TreeNode, source: TreeNode) {
|
||||||
for (const node of source.contents || []) {
|
for (const node of source.contents || []) {
|
||||||
const dup = target.contents?.find($node => $node.path === node.path)
|
const dup = target.contents?.find($node => $node.path === node.path)
|
||||||
if (dup) {
|
if (dup) {
|
||||||
mergeNodes(dup, node)
|
inPlaceMergeNodes(dup, node)
|
||||||
} else {
|
} else {
|
||||||
if (!target.contents) target.contents = []
|
if (!target.contents) target.contents = []
|
||||||
target.contents.push(node)
|
target.contents.push(node)
|
||||||
|
|
@ -31,6 +31,14 @@ export class BaseLayer {
|
||||||
this.defer = defer
|
this.defer = defer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateNode = async (path: string, updateNode: (node: TreeNode) => void) => {
|
||||||
|
const node = await findNode(this.baseRoot, path)
|
||||||
|
if (node) {
|
||||||
|
updateNode(node)
|
||||||
|
this.baseHub.emit('emit', this.baseRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadTreeData = async (path: string) => {
|
loadTreeData = async (path: string) => {
|
||||||
const node = await findNode(this.baseRoot, path)
|
const node = await findNode(this.baseRoot, path)
|
||||||
if (node && node.type !== 'tree') return node
|
if (node && node.type !== 'tree') return node
|
||||||
|
|
@ -39,7 +47,7 @@ export class BaseLayer {
|
||||||
|
|
||||||
this.loading.add(path)
|
this.loading.add(path)
|
||||||
this.baseHub.emit('loadingChange', this.loading)
|
this.baseHub.emit('loadingChange', this.loading)
|
||||||
mergeNodes(this.baseRoot, await this.getTreeData(path))
|
inPlaceMergeNodes(this.baseRoot, await this.getTreeData(path))
|
||||||
this.loading.delete(path)
|
this.loading.delete(path)
|
||||||
this.baseHub.emit('loadingChange', this.loading)
|
this.baseHub.emit('loadingChange', this.loading)
|
||||||
this.baseHub.emit('emit', this.baseRoot)
|
this.baseHub.emit('emit', this.baseRoot)
|
||||||
|
|
|
||||||
7
src/utils/findMapFirst.ts
Normal file
7
src/utils/findMapFirst.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const findMapFirst = <T, R>(array: T[], map: (item: T) => R): R | null => {
|
||||||
|
for (const item of array) {
|
||||||
|
const result = map(item)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
11
src/utils/map.ts
Normal file
11
src/utils/map.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const map = <K, R>(
|
||||||
|
iterable: Iterable<K>,
|
||||||
|
callback: (item: K, index: number, iterable: Iterable<K>) => R,
|
||||||
|
): R[] => {
|
||||||
|
const result: R[] = []
|
||||||
|
let index = 0
|
||||||
|
for (const item of iterable) {
|
||||||
|
result.push(callback(item, index++, iterable))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -72,6 +72,9 @@ function createConfig({ envTarget }: { envTarget: Target }) {
|
||||||
manifest.manifest_version = 3
|
manifest.manifest_version = 3
|
||||||
// Firefox does not support service worker
|
// Firefox does not support service worker
|
||||||
Reflect.set(manifest.background, 'scripts', [manifest.background.service_worker])
|
Reflect.set(manifest.background, 'scripts', [manifest.background.service_worker])
|
||||||
|
Reflect.set(manifest, 'browser_specific_settings', {
|
||||||
|
gecko: { id: '{983bd86b-9d6f-4394-92b8-63d844c4ce4c}' },
|
||||||
|
})
|
||||||
Reflect.deleteProperty(manifest.background, 'service_worker')
|
Reflect.deleteProperty(manifest.background, 'service_worker')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -142,18 +145,23 @@ function createConfig({ envTarget }: { envTarget: Target }) {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
loader: 'babel-loader',
|
loader: 'swc-loader',
|
||||||
include: [srcPath],
|
include: [srcPath],
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
sideEffects: false,
|
sideEffects: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.[cm]?js$/,
|
test: /\.[cm]?js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'swc-loader',
|
||||||
// Transpile as least files under node_modules
|
// Transpile as least files under node_modules
|
||||||
include: /node_modules\/(webext-.*|superstruct)\/.*\.[cm]?js$/,
|
include: /node_modules\/(webext-.*|superstruct)\/.*\.[cm]?js$/,
|
||||||
options: {
|
options: {
|
||||||
cacheDirectory: true,
|
jsc: {
|
||||||
|
parser: {
|
||||||
|
syntax: 'ecmascript',
|
||||||
|
},
|
||||||
|
target: 'es2022',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue