Skip to content

Commit

Permalink
test: add integration tests using cypress and a custom terminal view
Browse files Browse the repository at this point in the history
  • Loading branch information
mikavilpas committed Jun 30, 2024
1 parent 05849f3 commit 29d593b
Show file tree
Hide file tree
Showing 38 changed files with 6,478 additions and 315 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ jobs:
with:
nvim_version: ${{ matrix.neovim_version }}
luarocks_version: "3.11.1"

# Install npm dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v6
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ scripts/lazy.nvim/
/lua_modules
/.luarocks
.repro
integration-tests/server/build/server.js
integration-tests/pid.txt
integration-tests/server/build
*.pem
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
registry = "https://registry.npmjs.org"
save-exact=true
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.9.0
21.7.3
4 changes: 4 additions & 0 deletions .prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
---
proseWrap: always
semi: false
plugins:
- prettier-plugin-organize-imports
91 changes: 91 additions & 0 deletions integration-tests/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @ts-check

/**
* @type {import('eslint').Linter.Config}
*/
const config = {
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict-type-checked",
"prettier",
],
plugins: ["eslint-plugin-no-only-tests"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: [
"tsconfig.json",
"./client/tsconfig.json",
"./cypress/tsconfig.json",
],
},
ignorePatterns: ["vite.config.js", "cypress.config.ts"],
rules: {
"no-only-tests/no-only-tests": "error",
"no-restricted-syntax": [
"error",
{
// Disable all typescript enum usage.
// Rationale: they have surprising issues, including:
// - cyclic dependency issues
// - enums without an explicit value depend on their initialization
// order (if someone changes the order of variants, it's a breaking
// change)
//
// Instead of enums:
// - use a union type (e.g. `type MyType = 'a' | 'b'`)
// - use an object with a constant (narrow) type (e.g. `const MyType = { a: 'a', b: 'b' } as const`)
//
// https://github.com/typescript-eslint/typescript-eslint/issues/561#issuecomment-496664453
selector: "TSEnumDeclaration",
message: "Don't declare enums",
},
],

// Automatically use `import type` for types (can simplify transpilation as type imports are never bundled in)
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-import-type-side-effects": "error",

// Require explicit return and argument types on exported functions' and classes' public class methods.
// Explicit types for function return values and arguments makes it clear
// to any calling code what is the module boundary's input and output.
// Adding explicit type annotations for those types can help improve code
// readability. It can also improve TypeScript type checking performance
// on larger codebases.
"@typescript-eslint/explicit-module-boundary-types": ["warn"],

// Disable shadowing variables (prevents some bugs)
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],

"lines-between-class-members": [
"error",
"always",
{
exceptAfterSingleLine: true,
},
],
// Functions cannot be empty, except for constructors (helps not forgetting to implement a function)
"no-empty-function": [
"error",
{
allow: ["constructors"],
},
],

// make sure no `catch` blocks are skipped because a returned promise is not awaited (this is a common mistake)
"no-return-await": "off",
"@typescript-eslint/return-await": "error",
"no-useless-constructor": "off",
"no-void": [
"error",
{
allowAsStatement: true,
},
],
// use the typescript rule instead
"@typescript-eslint/no-unused-vars": "off",
},
}

module.exports = config
24 changes: 24 additions & 0 deletions integration-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
2 changes: 2 additions & 0 deletions integration-tests/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
registry = "https://registry.npmjs.org"
save-exact=true
102 changes: 102 additions & 0 deletions integration-tests/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import "@xterm/xterm/css/xterm.css"
import "./style.css"

import { flavors } from "@catppuccin/palette"
import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import io from "socket.io-client"
import type {
StartAppMessage,
StdinMessage,
StdoutMessage,
} from "../server/server"
import "./startAppGlobalType"
import type { StartAppMessageArguments } from "./startAppGlobalType"

const app = document.querySelector<HTMLDivElement>("#app")
if (!app) {
throw new Error("No app element found")
}

const terminal = new Terminal({
cursorBlink: false,
convertEol: true,
fontSize: 13,
// letterSpacing: 0.5,
})
{
const colors = flavors.macchiato.colors
terminal.options.theme = {
background: colors.base.hex,
black: colors.crust.hex,
brightBlack: colors.surface2.hex,
blue: colors.blue.hex,
brightBlue: colors.blue.hex,
brightCyan: colors.sky.hex,
brightRed: colors.maroon.hex,
brightYellow: colors.yellow.hex,
cursor: colors.text.hex,
cyan: colors.sky.hex,
foreground: colors.text.hex,
green: colors.green.hex,
magenta: colors.lavender.hex,
red: colors.red.hex,
white: colors.text.hex,
yellow: colors.yellow.hex,
}

// The FitAddon makes the terminal fit the size of the container, the entire
// page in this case
const fitAddon = new FitAddon()
terminal.loadAddon(fitAddon)
terminal.open(app)
fitAddon.fit()

window.addEventListener("resize", () => {
fitAddon.fit()
})
}

// The client application runs in the browser. The terminal application runs on
// the server. The client connects to the server using a WebSocket, constantly
// sending and receiving data.
//
// The terminal application's output is sent to the client, and the client's
// input is sent to the terminal application.
const socket = io("ws://localhost:3000/")

socket.on("connect_error", (err) => {
console.error(`connect_error: `, err.message)
})

socket.on("disconnect", (reason) => {
console.log("disconnected: ", reason)
})

window.startApp = function startApp(args: StartAppMessageArguments) {
socket.emit("startApp" satisfies StartAppMessage, args)
}

socket.on(
"stdout" satisfies StdoutMessage,
(data: unknown, acknowledge: unknown) => {
if (typeof data !== "string") {
console.warn(`unexpected stdout message type: '${JSON.stringify(data)}'`)
return
}

if (typeof acknowledge !== "function") {
console.warn(
`unexpected callback message type: '${JSON.stringify(acknowledge)}'`,
)
return
}

terminal.write(data)
acknowledge()
},
)

terminal.onKey((event) => {
socket.emit("stdin" satisfies StdinMessage, event.key)
})
12 changes: 12 additions & 0 deletions integration-tests/client/startAppGlobalType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type StartAppMessageArguments = {
command: string
args: string[]
}

declare global {
interface Window {
startApp(args: StartAppMessageArguments): void
}
}

export {}
26 changes: 26 additions & 0 deletions integration-tests/client/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@font-face {
font-family: "DejaVuSansMNerdFontMono";
src: url("/DejaVuSansMNerdFontMono-Regular.ttf");
font-weight: normal;
font-style: normal;
}

:root {
color-scheme: dark;
background-color: black;
}

#app {
display: flex;
height: 100vh;
width: 100vw;
}

* {
font-family: "DejaVuSansMNerdFontMono", monospace;
}

body {
overflow-y: hidden;
overflow-x: hidden;
}
22 changes: 22 additions & 0 deletions integration-tests/client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
1 change: 1 addition & 0 deletions integration-tests/client/typescript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions integration-tests/client/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
9 changes: 9 additions & 0 deletions integration-tests/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "cypress"

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
})
11 changes: 11 additions & 0 deletions integration-tests/cypress/e2e/healthcheck.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
describe("the healthcheck", () => {
it("can run the :healthcheck for yazi.nvim", () => {
cy.visit("http://localhost:5173")
cy.startNeovim()

cy.typeIntoTerminal(":checkhealth yazi{enter}")

// the `yazi` application should be found successfully
cy.contains("OK yazi")
})
})
14 changes: 14 additions & 0 deletions integration-tests/cypress/e2e/opening-directories.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
describe("opening directories", () => {
it("can open a directory when starting with `neovim .`", () => {
cy.visit("http://localhost:5173")
cy.startNeovim({
// `neovim .` specifies to open the current directory when neovim is
// starting
filename: ".",
})

// yazi should now be visible, showing the names of adjacent files
cy.contains("file.txt")
cy.contains("initial-file.txt")
})
})
Loading

0 comments on commit 29d593b

Please sign in to comment.