Skip to content

Commit ddf25f6

Browse files
authored
Add cypress (#57)
* Add Cypress tests and GitHub action
1 parent 96ed632 commit ddf25f6

File tree

16 files changed

+2020
-111
lines changed

16 files changed

+2020
-111
lines changed

.github/workflows/build.yml

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,95 @@
1-
name: Build
2-
run-name: Build the project
1+
name: Build and Test
2+
run-name: Build and Test
33
on:
4+
workflow_dispatch:
45
push:
5-
branches: ["main"]
6+
branches:
7+
- "main"
8+
- "dev"
69
pull_request:
7-
branches: ["main"]
10+
branches:
11+
- "main"
12+
- "dev"
13+
paths:
14+
- ".github/workflows/build.yml"
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
18+
cancel-in-progress: true
19+
20+
defaults:
21+
run:
22+
shell: bash
23+
824
jobs:
9-
build:
25+
test:
26+
runs-on: ${{ matrix.os }}
1027
strategy:
1128
fail-fast: false
1229
matrix:
13-
include:
14-
- name: Linux
15-
os: ubuntu-latest
16-
# - name: macOS
17-
# os: macos-latest
18-
# - name: Windows
19-
# os: windows-latest
20-
name: ${{ matrix.name }}
21-
runs-on: ${{ matrix.os }}
30+
os: [ubuntu-latest, macos-latest]
31+
browser: [electron, chrome]
2232
steps:
23-
- uses: actions/checkout@v4
24-
- uses: actions/setup-node@v4
25-
- uses: leanprover/lean-action@v1
33+
- name: Checkout repository
34+
uses: actions/checkout@v4
35+
36+
- name: Setup Node.js
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: "22"
40+
41+
- name: Build Projects/mathlib-demo
42+
uses: leanprover/lean-action@v1
2643
with:
27-
lake-package-directory: "Projects/mathlib-demo"
44+
lake-package-directory: Projects/mathlib-demo
45+
use-github-cache: false
46+
auto-config: false
2847
build: true
2948
test: false
3049
lint: false
31-
- run: npm install
32-
# - run: npm audit
33-
# continue-on-error: true
34-
- run: npm run build_client
35-
- run: npm run build_server
50+
51+
- name: Build Projects/stable
52+
uses: leanprover/lean-action@v1
53+
with:
54+
lake-package-directory: Projects/stable
55+
use-github-cache: false
56+
auto-config: false
57+
build: true
58+
test: false
59+
lint: false
60+
61+
- name: Install dependencies
62+
run: npm ci
63+
64+
- name: Build client for production
65+
run: npm run build:client
66+
67+
- name: Run Cypress tests (No Video)
68+
id: cypress_no_video
69+
run: npm test
70+
env:
71+
CYPRESS_DEFAULT_BROWSER: ${{ matrix.browser }}
72+
CYPRESS_VIDEO: false
73+
74+
- name: Upload screenshots on failure
75+
if: failure() && steps.cypress_no_video.conclusion == 'failure'
76+
uses: actions/upload-artifact@v4
77+
with:
78+
name: cypress-screenshots-${{ matrix.os }}-${{ matrix.browser }}
79+
path: cypress/screenshots
80+
81+
- name: Run Cypress tests (With Video)
82+
id: cypress_with_video
83+
if: failure() && steps.cypress_no_video.conclusion == 'failure'
84+
run: npm test
85+
env:
86+
CYPRESS_DEFAULT_BROWSER: ${{ matrix.browser }}
87+
CYPRESS_VIDEO: true
88+
CYPRESS_VIDEO_COMPRESSION: true
89+
90+
- name: Upload videos
91+
if: failure() && steps.cypress_no_video.conclusion == 'failure'
92+
uses: actions/upload-artifact@v4
93+
with:
94+
name: cypress-videos-${{ matrix.os }}-${{ matrix.browser }}
95+
path: cypress/videos

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ dist
1212
dist-ssr
1313
*.local
1414

15+
cypress/screenshots
16+
cypress/videos
17+
1518
**/.lake
1619

1720
# Editor directories and files

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
engine-strict=true

Projects/mathlib-demo/lake-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "git",
66
"subDir": null,
77
"scope": "",
8-
"rev": "a69bc35b7028de551fb3c101d72f5ddbef2a6b2d",
8+
"rev": "0ae30b68e680199367b2d4de739cbfdd6032b480",
99
"name": "mathlib",
1010
"manifestFile": "lake-manifest.json",
1111
"inputRev": "master",

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ SHELL=/usr/bin/bash
100100
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/home/USER/.elan/bin:/home/USER/.nvm/versions/node/v20.8.0/bin/
101101
102102
# Update server (i.e. mathlib) of lean4web and delete mathlib cache
103-
* */6 * * * cd /home/USER/lean4web && npm run build_server 2>&1 1>/dev/null | logger -t lean4web
103+
* */6 * * * cd /home/USER/lean4web && npm run build:server 2>&1 1>/dev/null | logger -t lean4web
104104
40 2 * * * rm -rf /home/USER/.cache/mathlib/
105105
```
106106

@@ -144,7 +144,7 @@ for people with javascript disabled!
144144
Install [npm](https://www.npmjs.com/) and clone this repository. Inside the repository, run:
145145

146146
1. `npm install`, to install dependencies
147-
2. `npm run build_server`, to build contained lean projects under `Projects/` (or run `lake build` manually inside any lean project)
147+
2. `npm run build:server`, to build contained lean projects under `Projects/` (or run `lake build` manually inside any lean project)
148148
3. `npm start`, to start the server.
149149

150150
The project can be accessed via http://localhost:3000. (Internally, websocket requests to `ws://localhost:3000/`websockets will be forwarded to a Lean server running on port 8080.)

cypress.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "cypress";
2+
3+
export default defineConfig({
4+
defaultCommandTimeout: 15000,
5+
retries: 2,
6+
e2e: {
7+
baseUrl: "http://localhost:3000",
8+
setupNodeEvents(on, config) {
9+
// implement node event listeners here
10+
},
11+
},
12+
});

cypress/e2e/spec.cy.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
describe('The Editor', () => {
2+
it('displays the version selection', () => {
3+
cy.visit('/')
4+
cy.get("nav>*>select[name='leanVersion']").select('mathlib-demo')
5+
cy.iframe().contains('mathlib-demo.lean').should('exist')
6+
cy.get('.dropdown>.nav-link>.fa-bars').click()
7+
cy.contains('.nav-link', 'Lean Info').click()
8+
cy.containsAll('.modal', [
9+
'mathlibLatest',
10+
'leanprover/lean4',
11+
'mathlib',
12+
'aesop'
13+
]).find('.modal-close').click()
14+
15+
cy.get("nav>*>select[name='leanVersion']").select('stable')
16+
cy.iframe().contains('stable.lean').should('exist')
17+
cy.get('.dropdown>.nav-link>.fa-bars').click()
18+
cy.contains('.nav-link', 'Lean Info').click()
19+
cy.containsAll('.modal', [
20+
'leanprover/lean4:stable',
21+
'(no dependencies)'
22+
]).find('.modal-close').click()
23+
})
24+
25+
it('handles unicode correctly', () => {
26+
cy.visit('/')
27+
cy.get('div.view-lines').type('\\Omega = \\sum')
28+
cy.contains('div.view-lines', 'Ω = ∑').should('have.attr', 'style').and('include', 'JuliaMono')
29+
cy.get('.squiggly-error').should('exist')
30+
cy.iframe().contains('unexpected identifier; expected command').should('exist')
31+
})
32+
33+
it('handles input from url', () => {
34+
cy.visit('/#codez=pXAAILwoiERA')
35+
cy.contains('div.view-line', 'Ω = ∑').should('exist')
36+
cy.get('.squiggly-error').should('exist')
37+
cy.iframe().contains('unexpected identifier; expected command').should('exist')
38+
})
39+
40+
it('loads example correctly on mobile', () => {
41+
cy.viewport(550, 750)
42+
cy.visit('/')
43+
cy.get('.dropdown>.nav-link>.fa-bars').click()
44+
cy.contains('.dropdown .dropdown', 'Examples').click()
45+
cy.contains('.nav-link', 'Logic').click()
46+
cy.containsAll(['import Mathlib.Logic.Basic', 'variable (P Q : Prop)']).should('exist')
47+
})
48+
49+
it('loads example correctly on desktop', () => {
50+
cy.visit('/')
51+
cy.contains('.nav-link', 'Examples').click()
52+
cy.contains('.nav-link', 'Logic').click()
53+
cy.containsAll(['import Mathlib.Logic.Basic', 'variable (P Q : Prop)']).should('exist')
54+
})
55+
56+
it('displays correct infoview state', () => {
57+
cy.visit('/')
58+
cy.get('div.view-lines').type('example (P: Prop) : P \\or \\not P := by ')
59+
60+
cy.iframe().containsAll([
61+
'Tactic state',
62+
'1 goal',
63+
'P : Prop',
64+
'⊢ P ∨ ¬P',
65+
'unexpected end of input'
66+
]).should('exist')
67+
68+
cy.contains('div.view-lines', 'P := by').type('exact Classical.em P')
69+
cy.iframe().containsAll('details', ['Tactic state', 'No goals']).should('exist')
70+
cy.iframe().contains('details', 'Expected type').should('not.be.open').click()
71+
cy.iframe().containsAll('details', ['P : Prop', '⊢ Prop']).should('be.open')
72+
})
73+
74+
it('displays correct hover tooltips', () => {
75+
cy.visit('/#codez=KYDwhgtgDgNsAEAKACgLnsgTgeygSnnWXkAoieAGo0IF54AjAT3lDAGMAXeAYRjAGc+AS1ZgYAOmAQMQA')
76+
cy.iframe().contains('Expected type').should('exist')
77+
cy.get('.cgmr.codicon').should('not.exist')
78+
cy.get('div.view-lines').realClick()
79+
cy.iframe().contains('No goals').should('exist')
80+
81+
cy.contains('div.view-line span', 'example').should('exist').then(($el) => cy.wrap($el).realHover())
82+
cy.contains('div.monaco-hover-content', '_example (P : Prop) : P ∨ ¬P').should('be.visible')
83+
84+
cy.contains('div.view-line span', 'by').should('exist').then(($el) => cy.wrap($el).realHover())
85+
cy.contains(
86+
'div.monaco-hover-content',
87+
'by tac constructs a term of the expected type by running the tactic(s) tac.'
88+
).should('be.visible')
89+
})
90+
91+
it('displays and handles code completion', () => {
92+
cy.visit('/')
93+
cy.iframe().contains('All Messages (0)').should('exist')
94+
cy.get('.cgmr.codicon').should('not.exist')
95+
cy.get('div.view-line').type('example (P: Prop) : P \\or \\not P := by appl', { delay: 100 })
96+
cy.containsAll('div.monaco-editor', ['by appl', 'apply?']).should('exist')
97+
.then(() => {
98+
cy.contains('div.view-line', 'by appl').type('{downArrow}{shift+enter}', { delay: 100 })
99+
cy.contains('div.view-line', 'apply?').should('exist')
100+
})
101+
cy.get('.squiggly-info').should('exist')
102+
})
103+
104+
it('displays and accetps quickfixes inline', () => {
105+
const modBtn = Cypress.platform === 'darwin' ? 'Meta' : 'Control'
106+
cy.visit('/')
107+
cy.get('div.view-line').type('example (P: Prop) : P \\or \\not P := by apply?')
108+
cy.contains('div.view-line', 'apply?').should('exist')
109+
cy.get('.squiggly-info').should('exist')
110+
cy.realPress([modBtn, '.'])
111+
cy.contains('.action-widget', 'Try this: exact Classical.em P').should('exist')
112+
cy.realPress([modBtn, '.'])
113+
cy.contains('div.view-line', 'exact Classical.em P').should('exist')
114+
})
115+
116+
it('displays and accetps suggestions from infoview', () => {
117+
cy.visit('/')
118+
cy.get('div.view-line').type('example (P: Prop) : P \\or \\not P := by apply?')
119+
cy.iframe().contains("span[title='Apply suggestion']", 'exact Classical.em P').click()
120+
cy.contains('div.view-line', 'exact Classical.em P').should('exist')
121+
})
122+
123+
it('loads from Zulip message on desktop', () => {
124+
cy.fixture('zulip-msg-1.txt').as('zulipMsg')
125+
cy.visit('/')
126+
cy.frameLoaded()
127+
cy.contains('.nav-link', 'Load').click()
128+
cy.contains('.nav-link', 'Load Zulip Message').click()
129+
cy.get('@zulipMsg').should('include', 'An example snippet').then(($msg) => {
130+
cy.get('.modal>form>textarea').invoke('val', $msg).then(() => {
131+
cy.get(".modal>form>input[value='Parse message']").should('exist').click()
132+
})
133+
})
134+
cy.containsAll('.view-lines', ['#eval Lean.toolchain', '#check Classical.em']).should('exist')
135+
cy.iframe().contains('Restart File').should('exist').click()
136+
cy.iframe().contains('details', 'All Messages (2)').should('exist').click()
137+
cy.iframe().containsAll('body', [
138+
'All Messages (2)',
139+
'leanprover/lean4',
140+
'Classical.em (p : Prop) : p ∨ ¬p'
141+
]).should('exist')
142+
})
143+
144+
it('handles custom lead character', () => {
145+
cy.visit('/')
146+
cy.iframe().contains('All Messages (0)').should('exist')
147+
cy.get('.cgmr.codicon').should('not.exist')
148+
cy.get('.dropdown>.nav-link>.fa-bars').click()
149+
cy.contains('.nav-link', 'Settings').click()
150+
cy.get('input#abbreviationCharacter').type(`{backspace}\${enter}`)
151+
cy.iframe().contains('All Messages (0)').should('exist')
152+
cy.get('.cgmr.codicon').should('not.exist')
153+
cy.get('div.view-line').type('example ($tau) : $tau $or $not $tau := by exact Classical.em $tau ')
154+
cy.contains('div.view-line', 'example (τ) : τ ∨ ¬ τ := by exact Classical.em τ').should('exist')
155+
cy.iframe().contains('No goals').should('exist').then(() => {
156+
cy.contains('div.view-line span', 'τ').realHover({ position: "center" })
157+
cy.contains(
158+
'div.monaco-hover-content',
159+
'Type τ using $ta or $tau'
160+
).should('be.visible')
161+
})
162+
})
163+
164+
it('can switch themes', () => {
165+
cy.visit('/')
166+
cy.iframe().contains('All Messages (0)').should('exist')
167+
cy.get('.monaco-editor').should('exist').then(($editor) => {
168+
const oldBg = $editor.css('background-color')
169+
cy.get('.dropdown>.nav-link>.fa-bars').click()
170+
cy.contains('.nav-link', 'Settings').click()
171+
cy.get("select#theme option").not(':selected').first().invoke('val').then(($newVal) => {
172+
cy.get("select#theme").select($newVal).should(() => {
173+
const newBg = $editor.css('background-color')
174+
expect(newBg).to.not.eq(oldBg)
175+
})
176+
})
177+
})
178+
})
179+
180+
it('shows correct go-to definitions', () => {
181+
const isOnDarwin = Cypress.platform === 'darwin'
182+
const alertShown = cy.stub().as("alertShown")
183+
cy.on('window:confirm', alertShown)
184+
cy.visit('/')
185+
cy.get('div.view-line').type('#check Classical.em')
186+
cy.contains('div.view-line span', 'Classical.em').as('defToCheck').should('exist')
187+
cy.get('@defToCheck').realClick({ ctrlKey: !isOnDarwin, metaKey: isOnDarwin })
188+
cy.get("@alertShown").should("have.been.calledOnce")
189+
.and('have.been.calledWithMatch', /Do you want to open the docs\?\s+\/Init\/Classical/gm)
190+
})
191+
})

cypress/fixtures/zulip-msg-1.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
An example snippet
2+
```lean
3+
#eval Lean.toolchain
4+
#check Classical.em
5+
```
6+
which should be parsed.
7+
8+
You could also consider `Cons : Set (Fin n) → pre_simple_graph n → pre_simple_graph (n + 1)`.

cypress/support/assertions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
chai.use(function (_chai, utils) {
2+
_chai.Assertion.addProperty('open', function() {
3+
const isOpen = utils.flag(this, 'object').attr('open') != null
4+
5+
this.assert(
6+
isOpen,
7+
'expected #{this} to be open but it was not',
8+
'expected #{this} to not be open but it was',
9+
isOpen
10+
)
11+
})
12+
})

0 commit comments

Comments
 (0)