Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding ui component tests #590

Merged
merged 28 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
860f8de
docs: additional cypress + vitest comparison content
JessicaSachs Dec 21, 2021
d9d4754
feat: ui testing
edimitchel Jan 19, 2022
4150f74
fix: seperate cypress tests
edimitchel Jan 19, 2022
7a60412
Merge branch 'vitest-dev:main' into main
JessicaSachs Jan 20, 2022
6c8ec2a
feat: adding cypress component testing for ui
JessicaSachs Jan 20, 2022
205e987
chore: share the common deps for global app setup
JessicaSachs Jan 20, 2022
b78b171
Merge remote-tracking branch 'origin/main' into jess/adding-cy-compon…
JessicaSachs Jan 20, 2022
d042221
spacing
JessicaSachs Jan 20, 2022
bd26fe8
Merge remote-tracking branch 'origin/main' into jess/adding-cy-compon…
JessicaSachs Jan 22, 2022
0f0cf9f
Update package.json
JessicaSachs Jan 22, 2022
ea4c8a5
chore: adding OptimizationPersist + PkgConfig to reduce flake
JessicaSachs Jan 23, 2022
9e0ab89
Merge branch 'jess/adding-cy-component-tests' of https://github.com/J…
JessicaSachs Jan 23, 2022
343ca1f
chore: workaround for unocss hmr
JessicaSachs Jan 23, 2022
c5317d8
chore: adding ts-ignore comments
JessicaSachs Jan 23, 2022
8fb64c2
chore: reordering data-testid
JessicaSachs Jan 23, 2022
c16d457
chore: ts-expect-error
JessicaSachs Jan 23, 2022
09bdb65
--allow-empty
JessicaSachs Jan 25, 2022
8c1269f
bug: reproduction of failing vite + cypress setup
JessicaSachs Jan 27, 2022
bc8514f
chore: adding Vite 2.9.0-beta.3 to cold-start stability issues for UI…
JessicaSachs Mar 17, 2022
c378713
Merge remote-tracking branch 'origin/main' into jess/adding-cy-compon…
JessicaSachs Mar 17, 2022
9a9c9df
chore: fixing types
JessicaSachs Mar 17, 2022
70b3359
chore: fixing types
JessicaSachs Mar 17, 2022
91fa5da
reenabling tests
JessicaSachs Mar 17, 2022
09cf89e
adding faker seed back in
JessicaSachs Mar 17, 2022
939ebdb
bumping faker version
JessicaSachs Mar 17, 2022
60b136b
Merge branch 'main' into jess/adding-cy-component-tests
antfu Mar 19, 2022
c8e8d61
Merge branch 'main' into jess/adding-cy-component-tests
antfu Mar 21, 2022
170f15a
Merge branch 'main' into jess/adding-cy-component-tests
antfu Mar 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Test
run: pnpm run test:ci

- name: Test UI
run: pnpm run ui:test

- name: Lint
run: pnpm run lint --if-present

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ dist
.idea
.DS_Store
bench/test/*/*/
cypress/videos
cypress/downloads
cypress/screenshots
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"test:ci": "cross-env CI=true pnpm -r --stream --filter !@vitest/monorepo --filter !@vitest/test-fails run test --",
"typecheck": "tsc --noEmit",
"ui:build": "vite build packages/ui",
"ui:dev": "vite packages/ui"
"ui:dev": "vite packages/ui",
"ui:test": "npm -C packages/ui run test:run"
},
"devDependencies": {
"@antfu/eslint-config": "^0.16.0",
Expand Down
23 changes: 23 additions & 0 deletions packages/ui/client/components/IconButton.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import IconButton from './IconButton.vue'

const title = 'A star'
const icon = 'i-carbon-star-filled'

describe('IconButton', () => {
it('should render the title', () => {
cy.mount(<IconButton m="2" title={title} icon={icon}/>)
.get(`[aria-label="${title}"][role=button]`)
.should('be.visible')
})

it('can be overridden with a slot', () => {
cy.mount(<IconButton m="2" icon={icon}>
<span text="32px">⭐️</span>
</IconButton>)
.get(`.${icon}`)
.should('not.exist')
.get('button')
.contains('⭐️')
.should('be.visible')
})
})
35 changes: 35 additions & 0 deletions packages/ui/client/components/dashboard/DashboardEntry.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import faker from '@faker-js/faker'
import DashboardEntry from './DashboardEntry.vue'

const body = () => (<div data-testid="body-content">{ faker.lorem.words(2) }</div>)
const header = () => (<div data-testid="header-content">{ faker.hacker.phrase() }</div>)
const bodySelector = '[data-testid=body-content]'
const headerSelector = '[data-testid=header-content]'
const tailSelector = '[data-testid=tail]'

// Used as a workaround until unocss HMR is compatible w Cy.
it('initial', () => {
cy.mount(<DashboardEntry v-slots={{ body, header }}/>)
})

describe('DashboardEntry', () => {
it('tail is rendered by default 2', () => {
cy.mount(<DashboardEntry v-slots={{ body, header }}/>)
.get(tailSelector)
.should('exist')
})

it('tail is not shown when true', () => {
cy.mount(<DashboardEntry tail v-slots={{ body, header }}/>)
.get(tailSelector)
.should('not.exist')
})

it('renders the body and header slots', () => {
cy.mount(<DashboardEntry v-slots={{ body, header }}/>)
.get(bodySelector)
.should('be.visible')
.get(headerSelector)
.should('be.visible')
})
})
2 changes: 1 addition & 1 deletion packages/ui/client/components/dashboard/DashboardEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ withDefaults(defineProps<{ tail?: boolean }>(), { tail: false })
<slot name="header" />
</div>
</div>
<div v-if="!tail" my-2 op50 w-1px bg-current origin-center rotate-15 translate-x-3 />
<div v-if="!tail" data-testid="tail" my-2 op50 w-1px bg-current origin-center rotate-15 translate-x-3 />
</div>
</template>
20 changes: 20 additions & 0 deletions packages/ui/client/components/dashboard/TestFilesEntry.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import TestFilesEntry from './TestFilesEntry.vue'

const entrySelector = '[data-testid=test-files-entry]'
const numFilesSelector = '[data-testid=num-files]'
const timingSelector = '[data-testid=run-time]'

describe('TestFilesEntry', () => {
it('renders the headers', () => {
cy.mount(<TestFilesEntry/>)
.get(entrySelector)
.should('contain.text', 'Files')
.and('contain.text', 'Time')

// Empty state
.get(timingSelector)
.should('have.text', '0ms')
.get(numFilesSelector)
.should('have.text', '0')
})
})
5 changes: 3 additions & 2 deletions packages/ui/client/components/dashboard/TestFilesEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { filesFailed, filesSnapshotFailed, filesSuccess, time } from '../../comp

<template>
<div
data-testid="test-files-entry"
grid="~ cols-[min-content_1fr_min-content]"
items-center gap="x-2 y-3" p="x4" relative font-light w-80
op80
>
<div i-carbon-document />
<div>Files</div>
<div class="number">
<div class="number" data-testid="num-files">
{{ files.length }}
</div>

Expand Down Expand Up @@ -45,7 +46,7 @@ import { filesFailed, filesSnapshotFailed, filesSuccess, time } from '../../comp

<div i-carbon-timer />
<div>Time</div>
<div class="number">
<div class="number" data-testid="run-time">
{{ time }}
</div>
</div>
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/client/components/dashboard/TestsEntry.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import TestsEntry from './TestsEntry.vue'

const passEntrySelector = '[data-testid=pass-entry]'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we share and export that testids from components?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen a pattern like that in a codebase, nor have I implemented it before because:

  1. The indirection makes debugging why a certain element wasn't found more difficult.

Instead of one step: Cmd+F data-testid="pass-entry-selector"

You'd need to do two: Cmd+F pass-entry-selector and then Cmd+F passEntrySelector to find its usage within the template.

  1. The boilerplate required makes the impl less readable.
  2. DataTestID is only used some of the time for selectors. [role="button"] and other selector strategies are also valid, and you'd create an inconsistency

For the implementation, I guess you would do:

<script lang="ts">
export const passEntryDataTestId = 'pass-entry-selector'
</script>

<script setup lang="ts">
// ....
</script>

<template>
	<div :data-testid="passEntryDataTestId" />
</template>
import Component, { passEntryDataTestId } from './Component.vue'
const passEntrySelector = `[data-testid=${passEntryDataTestId}]`
// test...

I would never recommend doing this. I hope this helps answer your question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... A bit of an off-topic aside, since you seem to be into the theory behind testing...


One of the patterns that isn't as common nowadays than it was for QA Engineers is the Page Object Model (POM) pattern. This pattern essentially has you build a Class representation of the "Pages" in your application. It was very common pre-2015 when writing Selenium tests.

// CheckoutForm.vue might have a POM that represents how to interact and get data on it.
class CheckoutForm {
  constructor(products) {
    products.forEach(() => addProductSomehow()) // either via UI or via some mock
  }

  products() {
    return cy.get('[data-testid=product]')
  }

  product(idx = 0) {
    return this.products.at(idx)
  }

  productRemoveButton(idx = 0) {
    return this.product(idx).findByLabel('Remove')
  }

  removeProduct(idx = 0) {
    return this.productRemoveButton(idx).click()
  }
}

and a test...

it('can remove products and checkout', () => {
  const products = [ /* two products */ ]
  const checkoutForm = new CheckoutForm(products)
  checkoutForm
    .products()
    .should('have.length', 2)
    .deleteProduct()
    .products()
    .should('have.length', 1)
})

Cypress doesn't really have good patterns for PageObjectModel (We actually discourage users from doing it, though I've never really agreed with why.) I've considered exploring this pattern again from the perspective of developers instead of QA Engineers, but haven't done so.

The downsides are a superset of the downsides that come with OOP. The upsides are the same. It promises to offer composition through actions and data. For example a CheckoutPage may have a CheckoutForm and you can compose with classes.

Anyway, this is just an exercise in thought-leadership -- it's an uncommon and antiquated pattern at the moment and I wouldn't recommend it for production usage.

😅

const failEntrySelector = '[data-testid=fail-entry]'
const totalEntrySelector = '[data-testid=total-entry]'
const todoEntrySelector = '[data-testid=todo-entry]'
const skippedEntrySelector = '[data-testid=skipped-entry]'

describe('TestsEntry', () => {
it('renders the headers for pass, fail, and total', () => {
cy.mount(<TestsEntry/>)
.get(passEntrySelector)
.should('contain.text', 'Pass')
.and('contain.text', '0')
.get(failEntrySelector)
.and('contain.text', 'Fail')
.and('contain.text', '0')
.get(totalEntrySelector)
.and('contain.text', 'Total')
.and('contain.text', '0')
})

it('does not render skipped and todo unless there are tests matched', () => {
cy.mount(<TestsEntry/>)
.get(skippedEntrySelector)
.should('not.exist')
.get(todoEntrySelector)
.should('not.exist')
})
})
10 changes: 5 additions & 5 deletions packages/ui/client/components/dashboard/TestsEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,39 @@ const pending = computed(() => {

<template>
<div flex="~ wrap" justify-evenly gap-2 p="x-4" relative>
<DashboardEntry text-green5>
<DashboardEntry text-green5 data-testid="pass-entry">
<template #header>
Pass
</template>
<template #body>
{{ pass }}
</template>
</DashboardEntry>
<DashboardEntry :class="{ 'text-red5': failed, 'op50': !failed }">
<DashboardEntry :class="{ 'text-red5': failed, 'op50': !failed }" data-testid="fail-entry">
<template #header>
Fail
</template>
<template #body>
{{ failed }}
</template>
</DashboardEntry>
<DashboardEntry v-if="skipped" op50>
<DashboardEntry v-if="skipped" op50 data-testid="skipped-entry">
<template #header>
Skip
</template>
<template #body>
{{ skipped }}
</template>
</DashboardEntry>
<DashboardEntry v-if="todo" op50>
<DashboardEntry v-if="todo" op50 data-testid="todo-entry">
<template #header>
Todo
</template>
<template #body>
{{ todo }}
</template>
</DashboardEntry>
<DashboardEntry :tail="true">
<DashboardEntry :tail="true" data-testid="total-entry">
<template #header>
Total
</template>
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/client/components/dashboard/TestsFilesContainer.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import TestsFilesContainer from './TestsFilesContainer.vue'

const entrySelector = '[data-testid=test-files-entry]'

describe('TestsFilesContainer', () => {
it('renders the TestEntry', () => {
cy.mount(<TestsFilesContainer/>)
.get(entrySelector)
.should('be.visible')
})
})
13 changes: 13 additions & 0 deletions packages/ui/client/components/views/ViewConsoleOutput.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ViewConsoleOutput from './ViewConsoleOutput.vue'

const entrySelector = '[data-testid=logs]'

describe('ViewConsoleOutput', () => {
it('renders', () => {
cy.mount(<ViewConsoleOutput/>)
.get(entrySelector)
// TODO: stub the websocket connection
// so that we can add logs and other data
.should('not.exist')
})
})
2 changes: 1 addition & 1 deletion packages/ui/client/components/views/ViewConsoleOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function getTaskName(id?: string) {
</script>

<template>
<div v-if="logs?.length" h-full class="scrolls" flex flex-col>
<div v-if="logs?.length" h-full class="scrolls" flex flex-col data-testid="logs">
<div v-for="log of logs" :key="log.taskId" font-mono>
<div border="b base" p-4>
<div
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/client/components/views/ViewEditor.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import faker from '@faker-js/faker'
import ViewEditor from './ViewEditor.vue'

const viewEditorSelector = '[data-testid=code-mirror]'

// TODO: stub out the rpc call in order to fully test this component
describe('ViewEditor', () => {
it('renders codemirror with line numbers', () => {
const file = {
filepath: faker.system.filePath(),
collectDuration: faker.time.recent(),
tasks: [],
}
cy.mount(<ViewEditor h="200px" file={file}/>)
.get(viewEditorSelector)
.type(`// ${faker.git.commitSha()}{enter}`, { delay: 0 })
.get(viewEditorSelector)
.should('contain.text', '1')
.and('contain.text', '2')
})
})
1 change: 1 addition & 0 deletions packages/ui/client/components/views/ViewEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ watch([cm, failed], () => {
h-full
v-bind="{ lineNumbers: true }"
:mode="ext"
data-testid="code-mirror"
@save="onSave"
/>
</template>
25 changes: 25 additions & 0 deletions packages/ui/client/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createRouter as _createRouter, createWebHistory } from 'vue-router'
import FloatingVue, { VTooltip } from 'floating-vue'
import routes from 'virtual:generated-pages'
import 'd3-graph-controller/default.css'
import 'splitpanes/dist/splitpanes.css'
import '@unocss/reset/tailwind.css'
import 'codemirror/lib/codemirror.css'
import 'codemirror-theme-vars/base.css'
import './styles/main.css'
import 'floating-vue/dist/style.css'
import 'uno.css'

export const directives = {
tooltip: VTooltip,
}

FloatingVue.options.instantMove = true
FloatingVue.options.distance = 10

export const createRouter = () => _createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})

export const plugins = [createRouter]
26 changes: 7 additions & 19 deletions packages/ui/client/main.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from 'virtual:generated-pages'
import FloatingVue, { VTooltip } from 'floating-vue'
import { directives, plugins } from './global-setup'
import App from './App.vue'

import 'd3-graph-controller/default.css'
import 'splitpanes/dist/splitpanes.css'
import '@unocss/reset/tailwind.css'
import 'codemirror/lib/codemirror.css'
import 'codemirror-theme-vars/base.css'
import 'floating-vue/dist/style.css'
import './styles/main.css'
import 'uno.css'

const app = createApp(App)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,

plugins.forEach((plugin) => {
app.use(plugin)
})
app.use(router)

app.directive('tooltip', VTooltip)
FloatingVue.options.instantMove = true
FloatingVue.options.distance = 10
Object.entries(directives).forEach(([name, directive]) => {
app.directive(name, directive)
})

app.mount('#app')
9 changes: 9 additions & 0 deletions packages/ui/client/pages/App.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from './index.vue'

// When mocking WebSockets, we'll want to follow the guide here
// https://glebbahmutov.com/blog/test-socketio-chat-using-cypress/#use-socketio-from-cypress
describe('App', () => {
it('should render', () => {
cy.mount(<App />)
})
})
7 changes: 7 additions & 0 deletions packages/ui/cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"testFiles": "**/*.cy.{js,ts,jsx,tsx}",
"componentFolder": "client",
"supportFile": "cypress/support/index.ts",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": false
}
15 changes: 15 additions & 0 deletions packages/ui/cypress/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from 'path'
import { startDevServer } from '@cypress/vite-dev-server'

const plugin: Cypress.PluginConfig = (on, config) => {
on('dev-server:start', options => startDevServer({
options,
viteConfig: {
configFile: path.resolve(__dirname, './vite.config.ts'),
},
}))

return config
}

export default plugin
7 changes: 7 additions & 0 deletions packages/ui/cypress/plugins/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { config } from '../../vite.config'

config.plugins?.push(vueJsx())

export default defineConfig(config)
10 changes: 10 additions & 0 deletions packages/ui/cypress/support/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import faker from '@faker-js/faker'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, happy to see we use it! 🙌

import '../../client/global-setup'

import { registerMount } from './mount'

before(() => {
faker.seed(0)
})

registerMount()