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:
Pierre Jacquier
2023-03-15 04:32:46 -04:00
committed by GitHub
parent 9208c46dac
commit f4fa083137
16 changed files with 3532 additions and 2365 deletions

View File

@ -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
View File

@ -23,3 +23,8 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
.env*

4636
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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/',
})

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
View 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
View 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

1016
yarn.lock

File diff suppressed because it is too large Load Diff