e2e tests (#17)
* Setup playwright for e2e tests Fixes #12 * Chromium * First working test, clean up * Merge actions * New headless mode * Clean up, bugfix * Bug fixes, cleaner sendMessage code * Rebase * Rebase * Load tokens and open public page * Test CI * Working test * Lint * Try to address flakyness * Clean up test * Comment * No export * More clean up * More clean up * Adds authorized pop up test * Adds comment * Add snapshots * New linux screenshots
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -24,6 +24,14 @@ jobs:
|
||||
|
||||
- run: yarn test
|
||||
|
||||
- name: Run playwright e2e tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GLOBAL_PAT}}
|
||||
KITTYCAD_TOKEN: ${{secrets.KITTYCAD_TOKEN}}
|
||||
run: |
|
||||
yarn playwright install chromium --with-deps
|
||||
yarn playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -23,3 +23,8 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
.env*
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false craco build",
|
||||
"test": "react-scripts test",
|
||||
"e2e": "yarn build && yarn playwright test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@ -68,6 +69,8 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.31.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
|
||||
30
playwright.config.ts
Normal file
30
playwright.config.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
})
|
||||
@ -18,8 +18,8 @@ import {
|
||||
} from './storage'
|
||||
import { getFileDiff } from './diff'
|
||||
|
||||
let github: Octokit
|
||||
let kittycad: Client
|
||||
let github: Octokit | undefined
|
||||
let kittycad: Client | undefined
|
||||
|
||||
async function initGithubApi() {
|
||||
try {
|
||||
@ -28,38 +28,45 @@ async function initGithubApi() {
|
||||
console.log(`Logged in on github.com as ${octokitResponse.data.login}`)
|
||||
} catch (e) {
|
||||
console.log('Couldnt initiate the github api client')
|
||||
github = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function initKittycadApi() {
|
||||
try {
|
||||
kittycad = new Client(await getStorageKittycadToken())
|
||||
const kittycadResponse = await users.get_user_self({ client: kittycad })
|
||||
console.log(
|
||||
`Logged in on kittycad.io as ${
|
||||
(kittycadResponse as KittycadUser).email
|
||||
}`
|
||||
)
|
||||
const response = await users.get_user_self({ client: kittycad })
|
||||
if ('error_code' in response) throw response
|
||||
const { email } = response
|
||||
if (!email) throw Error('Empty user, token is probably wrong')
|
||||
console.log(`Logged in on kittycad.io as ${email}`)
|
||||
} catch (e) {
|
||||
console.log("Couldn't initiate the kittycad api client")
|
||||
kittycad = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGithubTokenAndReload(token: string): Promise<void> {
|
||||
github = undefined
|
||||
await setStorageGithubToken(token)
|
||||
await initGithubApi()
|
||||
}
|
||||
|
||||
async function saveKittycadTokenAndReload(token: string): Promise<void> {
|
||||
kittycad = undefined
|
||||
await setStorageKittycadToken(token)
|
||||
await initKittycadApi()
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
// Delay to allow for external storage sets before auth, like in e2e
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
await initKittycadApi()
|
||||
await initGithubApi()
|
||||
})()
|
||||
|
||||
const noClientError = new Error('API client is undefined')
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(
|
||||
message: Message,
|
||||
@ -68,48 +75,68 @@ chrome.runtime.onMessage.addListener(
|
||||
) => {
|
||||
console.log(`Received ${message.id} from ${sender.id}`)
|
||||
if (message.id === MessageIds.GetGithubPullFiles) {
|
||||
if (!github) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
const { owner, repo, pull } =
|
||||
message.data as MessageGetGithubPullFilesData
|
||||
github.rest.pulls
|
||||
.listFiles({ owner, repo, pull_number: pull })
|
||||
.then(r => sendResponse(r.data))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.id === MessageIds.GetGithubPull) {
|
||||
if (!github) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
const { owner, repo, pull } =
|
||||
message.data as MessageGetGithubPullFilesData
|
||||
github.rest.pulls
|
||||
.get({ owner, repo, pull_number: pull })
|
||||
.then(r => sendResponse(r.data))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.id === MessageIds.GetGithubCommit) {
|
||||
if (!github) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
const { owner, repo, sha } =
|
||||
message.data as MessageGetGithubCommitData
|
||||
github.rest.repos
|
||||
.getCommit({ owner, repo, ref: sha })
|
||||
.then(r => sendResponse(r.data))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.id === MessageIds.GetGithubUser) {
|
||||
if (!github) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
github.rest.users
|
||||
.getAuthenticated()
|
||||
.then(r => sendResponse(r.data))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.id === MessageIds.GetKittycadUser) {
|
||||
if (!kittycad) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
users
|
||||
.get_user_self({ client: kittycad })
|
||||
.then(r => sendResponse(r as KittycadUser))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -117,7 +144,7 @@ chrome.runtime.onMessage.addListener(
|
||||
const { token } = message.data as MessageSaveToken
|
||||
saveGithubTokenAndReload(token)
|
||||
.then(() => sendResponse({ token }))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -125,16 +152,20 @@ chrome.runtime.onMessage.addListener(
|
||||
const { token } = message.data as MessageSaveToken
|
||||
saveKittycadTokenAndReload(token)
|
||||
.then(() => sendResponse({ token }))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.id === MessageIds.GetFileDiff) {
|
||||
if (!kittycad || !github) {
|
||||
sendResponse({ error: noClientError })
|
||||
return false
|
||||
}
|
||||
const { owner, repo, sha, parentSha, file } =
|
||||
message.data as MessageGetFileDiff
|
||||
getFileDiff(github, kittycad, owner, repo, sha, parentSha, file)
|
||||
.then(r => sendResponse(r))
|
||||
.catch(e => sendResponse(e))
|
||||
.catch(error => sendResponse({ error }))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,16 @@ async function injectDiff(
|
||||
}
|
||||
|
||||
for (const { element, file } of map) {
|
||||
const fileDiff = await chrome.runtime.sendMessage<Message, FileDiff>({
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
id: MessageIds.GetFileDiff,
|
||||
data: { owner, repo, sha, parentSha, file },
|
||||
})
|
||||
createRoot(element).render(React.createElement(CadDiff, fileDiff))
|
||||
if ('error' in response) {
|
||||
console.log(response.error)
|
||||
} else {
|
||||
const diff = response as FileDiff
|
||||
createRoot(element).render(React.createElement(CadDiff, diff))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +81,7 @@ gitHubInjection(async () => {
|
||||
const pullParams = getGithubPullUrlParams(url)
|
||||
if (pullParams) {
|
||||
const { owner, repo, pull } = pullParams
|
||||
console.log('Found PR diff: ', owner, repo, pull)
|
||||
await injectPullDiff(owner, repo, pull, window.document)
|
||||
return
|
||||
}
|
||||
@ -83,6 +89,7 @@ gitHubInjection(async () => {
|
||||
const commitParams = getGithubCommitUrlParams(url)
|
||||
if (commitParams) {
|
||||
const { owner, repo, sha } = commitParams
|
||||
console.log('Found commit diff: ', owner, repo, sha)
|
||||
await injectCommitDiff(owner, repo, sha, window.document)
|
||||
return
|
||||
}
|
||||
|
||||
@ -55,6 +55,10 @@ export type MessageSaveToken = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type MessageError = {
|
||||
error: Error
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
id: MessageIds
|
||||
data?:
|
||||
@ -72,5 +76,5 @@ export type MessageResponse =
|
||||
| KittycadUser
|
||||
| MessageSaveToken
|
||||
| FileDiff
|
||||
| Error
|
||||
| MessageError
|
||||
| void
|
||||
|
||||
@ -15,9 +15,8 @@ export function Settings() {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
id: MessageIds.GetGithubUser,
|
||||
})
|
||||
if (Object.keys(response).length === 0) throw Error('no response')
|
||||
const user = response as User
|
||||
setGithubUser(user)
|
||||
if ('error' in response) throw response.error
|
||||
setGithubUser(response as User)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setGithubUser(undefined)
|
||||
@ -29,9 +28,8 @@ export function Settings() {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
id: MessageIds.GetKittycadUser,
|
||||
})
|
||||
if (Object.keys(response).length === 0) throw Error('no response')
|
||||
const user = response as KittycadUser
|
||||
setKittycadUser(user)
|
||||
if ('error' in response) throw response.error
|
||||
setKittycadUser(response as KittycadUser)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setKittycadUser(undefined)
|
||||
|
||||
46
tests/extension.spec.ts
Normal file
46
tests/extension.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { test, expect } from './fixtures'
|
||||
|
||||
test('popup page', async ({ page, extensionId }) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/index.html`)
|
||||
await expect(page.locator('body')).toContainText('Enter a GitHub token')
|
||||
await expect(page.locator('body')).toContainText('Enter a KittyCAD token')
|
||||
})
|
||||
|
||||
test('authorized popup page', async ({
|
||||
page,
|
||||
extensionId,
|
||||
authorizedBackground,
|
||||
}) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/index.html`)
|
||||
await page.waitForSelector('button')
|
||||
await expect(page.locator('body')).toContainText('Sign out')
|
||||
await expect(page.locator('button')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('pull request diff with an .obj file', async ({
|
||||
page,
|
||||
authorizedBackground,
|
||||
}) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
|
||||
await page.goto('https://github.com/KittyCAD/kittycad.ts/pull/3/files')
|
||||
const element = await page.waitForSelector('.js-file-content canvas')
|
||||
await page.waitForTimeout(1000) // making sure the element fully settled in
|
||||
const screenshot = await element.screenshot()
|
||||
expect(screenshot).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('commit diff with an .obj file', async ({
|
||||
page,
|
||||
authorizedBackground,
|
||||
}) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
|
||||
await page.goto(
|
||||
'https://github.com/KittyCAD/kittycad.ts/commit/08b50ee5a23b3ae7dd7b19383f14bbd520079cc1'
|
||||
)
|
||||
const element = await page.waitForSelector('.js-file-content canvas')
|
||||
await page.waitForTimeout(1000) // making sure the element fully settled in
|
||||
const screenshot = await element.screenshot()
|
||||
expect(screenshot).toMatchSnapshot()
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
63
tests/fixtures.ts
Normal file
63
tests/fixtures.ts
Normal file
@ -0,0 +1,63 @@
|
||||
// From https://playwright.dev/docs/chrome-extensions#testing
|
||||
import {
|
||||
test as base,
|
||||
chromium,
|
||||
Worker,
|
||||
type BrowserContext,
|
||||
} from '@playwright/test'
|
||||
import path from 'path'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
export const test = base.extend<{
|
||||
context: BrowserContext
|
||||
extensionId: string
|
||||
background: Worker
|
||||
authorizedBackground: Worker
|
||||
}>({
|
||||
context: async ({}, use) => {
|
||||
const pathToExtension = path.join(__dirname, '..', 'build')
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
headless: false,
|
||||
args: [
|
||||
`--headless=new`, // headless mode that allows for extensions
|
||||
`--disable-extensions-except=${pathToExtension}`,
|
||||
`--load-extension=${pathToExtension}`,
|
||||
],
|
||||
})
|
||||
await use(context)
|
||||
await context.close()
|
||||
},
|
||||
background: async ({ context }, use) => {
|
||||
let [background] = context.serviceWorkers()
|
||||
if (!background)
|
||||
background = await context.waitForEvent('serviceworker')
|
||||
|
||||
// Wait for the chrome object to be available
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
await use(background)
|
||||
},
|
||||
authorizedBackground: async ({ background }, use) => {
|
||||
// Load the env tokens in storage for auth
|
||||
const githubToken = process.env.GITHUB_TOKEN
|
||||
const kittycadToken = process.env.KITTYCAD_TOKEN
|
||||
await background.evaluate(
|
||||
async ([githubToken, kittycadToken]) => {
|
||||
await chrome.storage.local.set({
|
||||
ktk: kittycadToken, // from src/chrome/storage.ts
|
||||
gtk: githubToken, // from src/chrome/storage.ts
|
||||
})
|
||||
},
|
||||
[githubToken, kittycadToken]
|
||||
)
|
||||
|
||||
// Wait for background auth
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
await use(background)
|
||||
},
|
||||
extensionId: async ({ background }, use) => {
|
||||
const extensionId = background.url().split('/')[2]
|
||||
await use(extensionId)
|
||||
},
|
||||
})
|
||||
export const expect = test.expect
|
||||
Reference in New Issue
Block a user