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

Add first class Javascript/Typescript support to the Mill build tool #4253

Merged
merged 12 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/modules/ROOT/pages/javascriptlib/testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ This page will discuss common topics around working with test suites using the M

include::partial$example/javascriptlib/testing/1-test-suite.adoc[]


== Test Dependencies

include::partial$example/javascriptlib/testing/2-test-deps.adoc[]
include::partial$example/javascriptlib/testing/2-test-deps.adoc[]

== Integration Suite with Cypress

include::partial$example/javascriptlib/testing/3-integration-suite-cypress.adoc[]

== Integration Suite with PlayWright

include::partial$example/javascriptlib/testing/3-integration-suite-playwright.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package build

import mill._, javascriptlib._

object client extends ReactScriptsModule

object server extends TypeScriptModule {

def npmDeps =
Seq("@types/cors@^2.8.17", "@types/express@^5.0.0", "cors@^2.8.5", "express@^4.21.1")

/** Bundle client as resource */
def resources = Task {
os.copy(client.bundle().path, Task.dest / "build")
super.resources() ++ Seq(PathRef(Task.dest))
}

override def forkEnv = super.forkEnv() + ("PORT" -> "4000")

object test extends TypeScriptTests with TestModule.Cypress {
def service = server
def port = "4000"
}
}

// Documentation for mill.example.javascriptlib
// In this example we demonstrate integration testing using cypress
// `mill server.test` will start the service on the speicifed port, run tests with configurations defined in cypress.config.ts
// and kill the service once completed

/** Usage

> mill server.test
...
...Server listening on port 4000
... app.cy.ts...
... All specs passed!...
...
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

function App() {
return (
<div className="App">
<header className="App-header">
<h1 data-testid="heading">Hello, Cypress & PlayWright</h1>
<p>Brought to you by ✨✨mill.✨✨</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app/App';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @ts-nocheck
import { defineConfig } from 'node_modules/cypress';

export default defineConfig({
e2e: {
specPattern: '**/e2e/*.cy.ts',
baseUrl: 'http://localhost:4000',
supportFile: false
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import express, {Express} from 'express';
import cors from 'cors';

const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle
const Client = require.resolve(`${Resources}/index.html`);

const app: Express = express();
const port = process.env.PORT || 3001;
const BuildPath = Client.replace(/index\.html$/, "");

app.use(cors());
app.use(express.json());

// Middleware to serve static files from the "build" directory
app.use(express.static(BuildPath));

app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

export default app;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
describe('React App', () => {
it('displays the heading', () => {
// Visit the base URL
cy.visit('/');

// Check if the heading is visible and contains "Hello, Cypress!"
cy.get('[data-testid="heading"]').should('be.visible').and('contain.text', 'Hello, Cypress & PlayWright');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package build

import mill._, javascriptlib._

object client extends ReactScriptsModule

object server extends TypeScriptModule {

def npmDeps =
Seq("@types/cors@^2.8.17", "@types/express@^5.0.0", "cors@^2.8.5", "express@^4.21.1")

/** Bundle client as resource */
def resources = Task {
os.copy(client.bundle().path, Task.dest / "build")
super.resources() ++ Seq(PathRef(Task.dest))
}

def forkEnv = super.forkEnv() + ("PORT" -> "3000")

object test extends TypeScriptTests with TestModule.PlayWright {
def service = server
def port = "6000"
}
}

// Documentation for mill.example.javascriptlib
// In this example we demonstrate integration testing using playwright
// `mill server.test` will start the service on the speicifed port, run tests with configurations defined in playwright.config.ts
// and kill the service once completed

/** Usage

> mill server.test
...
...Server listening on port 6000
...
...1 passed...
...
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

function App() {
return (
<div className="App">
<header className="App-header">
<h1 data-testid="heading">Hello, Cypress & PlayWright</h1>
<p>Brought to you by ✨✨mill.✨✨</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app/App';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {defineConfig} from '@playwright/test';
import * as glob from 'node_modules/glob';
import * as path from 'path';

const testFiles = glob.sync('**/playwright/*.test.ts', {absolute: true});

export default defineConfig({
testDir: './',
testMatch: testFiles.map(file => path.relative(process.cwd(), file)),
timeout: 30000,
retries: 1,
use: {
baseURL: 'http://localhost:6000',
headless: true,
trace: 'on-first-retry',
launchOptions: {
args: ['--explicitly-allowed-ports=6000']
},
channel: 'chrome', // Use the stable Chrome channel
},
projects: [
{
name: 'chromium',
use: {browserName: 'chromium'}
}
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import express, {Express} from 'express';
import cors from 'cors';

const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle
const Client = require.resolve(`${Resources}/index.html`);

const app: Express = express();
const port = process.env.PORT || 3001;
const BuildPath = Client.replace(/index\.html$/, "");

app.use(cors());
app.use(express.json());

// Middleware to serve static files from the "build" directory
app.use(express.static(BuildPath));

app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

export default app;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { test, expect } from 'node_modules/@playwright/test';

test.describe('React App', () => {
test('displays the heading', async ({ page }) => {
// Visit the base URL
await page.goto('/');

// Check if the heading is visible
const heading = page.locator('[data-testid="heading"]');
await expect(heading).toBeVisible();
await expect(heading).toHaveText('Hello, Cypress & PlayWright');
});
});
Loading
Loading