mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
refactor tests to include both react and typescript tests
This commit is contained in:
1
tools/modern-tests/apps/typescript/.gitignore
vendored
Normal file
1
tools/modern-tests/apps/typescript/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
1
tools/modern-tests/apps/typescript/.meteor/.gitignore
vendored
Normal file
1
tools/modern-tests/apps/typescript/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
7
tools/modern-tests/apps/typescript/.meteor/.id
Normal file
7
tools/modern-tests/apps/typescript/.meteor/.id
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
k0367lnvnclor.z6cmuyuro69u
|
||||
23
tools/modern-tests/apps/typescript/.meteor/packages
Normal file
23
tools/modern-tests/apps/typescript/.meteor/packages
Normal file
@@ -0,0 +1,23 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
# Check this file (and the other files in this directory) into your repository.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base # Packages every Meteor app needs to have
|
||||
mobile-experience # Packages for a great mobile UX
|
||||
mongo # The database Meteor supports right now
|
||||
reactive-var # Reactive variable for tracker
|
||||
|
||||
standard-minifier-css # CSS minifier run for production mode
|
||||
standard-minifier-js # JS minifier run for production mode
|
||||
es5-shim # ECMAScript 5 compatibility for older browsers
|
||||
ecmascript # Enable ECMAScript2015+ syntax in app code
|
||||
typescript # Enable TypeScript syntax in .ts and .tsx modules
|
||||
shell-server # Server-side component of the `meteor shell` command
|
||||
hot-module-replacement # Update client in development without reloading the page
|
||||
|
||||
|
||||
static-html # Define static page content in .html files
|
||||
react-meteor-data # React higher-order component for reactively tracking Meteor data
|
||||
zodern:types # Pull in type declarations from other Meteor packages
|
||||
2
tools/modern-tests/apps/typescript/.meteor/platforms
Normal file
2
tools/modern-tests/apps/typescript/.meteor/platforms
Normal file
@@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
1
tools/modern-tests/apps/typescript/.meteor/release
Normal file
1
tools/modern-tests/apps/typescript/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
68
tools/modern-tests/apps/typescript/.meteor/versions
Normal file
68
tools/modern-tests/apps/typescript/.meteor/versions
Normal file
@@ -0,0 +1,68 @@
|
||||
allow-deny@2.1.0
|
||||
autoupdate@2.0.1
|
||||
babel-compiler@7.12.1
|
||||
babel-runtime@1.5.2
|
||||
base64@1.0.13
|
||||
binary-heap@1.0.12
|
||||
boilerplate-generator@2.0.1
|
||||
caching-compiler@2.0.1
|
||||
callback-hook@1.6.1
|
||||
check@1.4.4
|
||||
core-runtime@1.0.0
|
||||
ddp@1.4.2
|
||||
ddp-client@3.1.1
|
||||
ddp-common@1.4.4
|
||||
ddp-server@3.1.2
|
||||
diff-sequence@1.1.3
|
||||
dynamic-import@0.7.4
|
||||
ecmascript@0.16.12
|
||||
ecmascript-runtime@0.8.3
|
||||
ecmascript-runtime-client@0.12.3
|
||||
ecmascript-runtime-server@0.11.1
|
||||
ejson@1.1.5
|
||||
es5-shim@4.8.1
|
||||
facts-base@1.0.2
|
||||
fetch@0.1.6
|
||||
geojson-utils@1.0.12
|
||||
hot-code-push@1.0.5
|
||||
hot-module-replacement@0.5.4
|
||||
id-map@1.2.0
|
||||
inter-process-messaging@0.1.2
|
||||
launch-screen@2.0.1
|
||||
logging@1.3.6
|
||||
meteor@2.1.1
|
||||
meteor-base@1.5.2
|
||||
minifier-css@2.0.1
|
||||
minifier-js@3.0.3
|
||||
minimongo@2.0.3
|
||||
mobile-experience@1.1.2
|
||||
mobile-status-bar@1.1.1
|
||||
modern-browsers@0.2.3
|
||||
modules@0.20.3
|
||||
modules-runtime@0.13.2
|
||||
modules-runtime-hot@0.14.3
|
||||
mongo@2.1.3
|
||||
mongo-decimal@0.2.0
|
||||
mongo-dev-server@1.1.1
|
||||
mongo-id@1.0.9
|
||||
npm-mongo@6.16.0
|
||||
ordered-dict@1.2.0
|
||||
promise@1.0.0
|
||||
random@1.2.2
|
||||
react-fast-refresh@0.2.9
|
||||
react-meteor-data@4.0.0
|
||||
reactive-var@1.0.13
|
||||
reload@1.3.2
|
||||
retry@1.1.1
|
||||
routepolicy@1.1.2
|
||||
shell-server@0.6.1
|
||||
socket-stream-client@0.6.1
|
||||
standard-minifier-css@1.9.3
|
||||
standard-minifier-js@3.1.1
|
||||
static-html@1.4.0
|
||||
static-html-tools@1.0.0
|
||||
tracker@1.3.4
|
||||
typescript@5.6.5
|
||||
webapp@2.0.7
|
||||
webapp-hashing@1.1.2
|
||||
zodern:types@1.0.13
|
||||
4
tools/modern-tests/apps/typescript/client/main.css
Normal file
4
tools/modern-tests/apps/typescript/client/main.css
Normal file
@@ -0,0 +1,4 @@
|
||||
body {
|
||||
padding: 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
8
tools/modern-tests/apps/typescript/client/main.html
Normal file
8
tools/modern-tests/apps/typescript/client/main.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<head>
|
||||
<title>typescript</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="react-target"></div>
|
||||
</body>
|
||||
10
tools/modern-tests/apps/typescript/client/main.tsx
Normal file
10
tools/modern-tests/apps/typescript/client/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { App } from '/imports/ui/App';
|
||||
|
||||
Meteor.startup(() => {
|
||||
const container = document.getElementById('react-target');
|
||||
const root = createRoot(container!);
|
||||
root.render(<App />);
|
||||
});
|
||||
10
tools/modern-tests/apps/typescript/imports/api/links.ts
Normal file
10
tools/modern-tests/apps/typescript/imports/api/links.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
export interface Link {
|
||||
_id?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export const LinksCollection = new Mongo.Collection<Link>('links');
|
||||
11
tools/modern-tests/apps/typescript/imports/ui/App.tsx
Normal file
11
tools/modern-tests/apps/typescript/imports/ui/App.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Hello } from './Hello';
|
||||
import { Info } from './Info';
|
||||
|
||||
export const App = () => (
|
||||
<div>
|
||||
<h1>Welcome to Meteor!</h1>
|
||||
<Hello />
|
||||
<Info />
|
||||
</div>
|
||||
);
|
||||
16
tools/modern-tests/apps/typescript/imports/ui/Hello.tsx
Normal file
16
tools/modern-tests/apps/typescript/imports/ui/Hello.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const Hello = () => {
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
const increment = () => {
|
||||
setCounter(counter + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={increment}>Click Me</button>
|
||||
<p>You've pressed the button {counter} times.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
tools/modern-tests/apps/typescript/imports/ui/Info.tsx
Normal file
27
tools/modern-tests/apps/typescript/imports/ui/Info.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { useFind, useSubscribe } from "meteor/react-meteor-data";
|
||||
import { LinksCollection, Link } from "../api/links";
|
||||
|
||||
export const Info = () => {
|
||||
const isLoading = useSubscribe("links");
|
||||
const links = useFind(() => LinksCollection.find());
|
||||
|
||||
if (isLoading()) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const makeLink = (link: Link) => {
|
||||
return (
|
||||
<li key={ link._id }>
|
||||
<a href={ link.url } target="_blank">{ link.title }</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Learn Meteor!</h2>
|
||||
<ul>{ links.map(makeLink) }</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1609
tools/modern-tests/apps/typescript/package-lock.json
generated
Normal file
1609
tools/modern-tests/apps/typescript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
tools/modern-tests/apps/typescript/package.json
Normal file
35
tools/modern-tests/apps/typescript/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "meteor run",
|
||||
"test": "meteor test --once --driver-package meteortesting:mocha",
|
||||
"test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha",
|
||||
"visualize": "meteor --production --extra-packages bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"meteor-node-stubs": "^1.2.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/meteor": "^2.9.9",
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/react": "^18.2.5",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"playwright": "^1.54.2",
|
||||
"ts-checker-rspack-plugin": "^1.1.5",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"meteor": {
|
||||
"mainModule": {
|
||||
"client": "client/main.tsx",
|
||||
"server": "server/main.ts"
|
||||
},
|
||||
"testModule": "tests/main.ts",
|
||||
"modern": true
|
||||
}
|
||||
}
|
||||
18
tools/modern-tests/apps/typescript/rspack.config.js
Normal file
18
tools/modern-tests/apps/typescript/rspack.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from '@meteorjs/rspack';
|
||||
import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin';
|
||||
|
||||
/**
|
||||
* Rspack configuration for Meteor projects.
|
||||
*
|
||||
* Provides typed flags on the `Meteor` object, such as:
|
||||
* - `Meteor.isClient` / `Meteor.isServer`
|
||||
* - `Meteor.isDevelopment` / `Meteor.isProduction`
|
||||
* - …and other flags available
|
||||
*
|
||||
* Use these flags to adjust your build settings based on environment.
|
||||
*/
|
||||
export default defineConfig(Meteor => {
|
||||
return {
|
||||
plugins: [new TsCheckerRspackPlugin()],
|
||||
};
|
||||
});
|
||||
37
tools/modern-tests/apps/typescript/server/main.ts
Normal file
37
tools/modern-tests/apps/typescript/server/main.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Link, LinksCollection } from '/imports/api/links';
|
||||
|
||||
async function insertLink({ title, url }: Pick<Link, 'title' | 'url'>) {
|
||||
await LinksCollection.insertAsync({ title, url, createdAt: new Date() });
|
||||
}
|
||||
|
||||
Meteor.startup(async () => {
|
||||
// If the Links collection is empty, add some data.
|
||||
if (await LinksCollection.find().countAsync() === 0) {
|
||||
await insertLink({
|
||||
title: 'Do the Tutorial',
|
||||
url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html',
|
||||
});
|
||||
|
||||
await insertLink({
|
||||
title: 'Follow the Guide',
|
||||
url: 'https://guide.meteor.com',
|
||||
});
|
||||
|
||||
await insertLink({
|
||||
title: 'Read the Docs',
|
||||
url: 'https://docs.meteor.com',
|
||||
});
|
||||
|
||||
await insertLink({
|
||||
title: 'Discussions',
|
||||
url: 'https://forums.meteor.com',
|
||||
});
|
||||
}
|
||||
|
||||
// We publish the entire Links collection to all clients.
|
||||
// In order to be fetched in real-time to the clients
|
||||
Meteor.publish("links", function () {
|
||||
return LinksCollection.find();
|
||||
});
|
||||
});
|
||||
21
tools/modern-tests/apps/typescript/tests/main.ts
Normal file
21
tools/modern-tests/apps/typescript/tests/main.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import assert from 'assert';
|
||||
|
||||
describe('typescript', function () {
|
||||
it('package.json has correct name', async function () {
|
||||
const { name } = await import('../package.json');
|
||||
assert.strictEqual(name, 'typescript');
|
||||
});
|
||||
|
||||
if (Meteor.isClient) {
|
||||
it('client is not server', function () {
|
||||
assert.strictEqual(Meteor.isServer, false);
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
it('server is not client', function () {
|
||||
assert.strictEqual(Meteor.isClient, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
49
tools/modern-tests/apps/typescript/tsconfig.json
Normal file
49
tools/modern-tests/apps/typescript/tsconfig.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2018",
|
||||
"module": "esNext",
|
||||
"lib": ["esnext", "dom"],
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
/* Support absolute /imports/* with a leading '/' */
|
||||
"/*": ["*"],
|
||||
/* Pull in type declarations for Meteor packages from either zodern:types or @types/meteor packages */
|
||||
"meteor/*": [
|
||||
"node_modules/@types/meteor/*",
|
||||
".meteor/local/types/packages.d.ts"
|
||||
]
|
||||
},
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "mocha"],
|
||||
"esModuleInterop": true,
|
||||
"preserveSymlinks": true
|
||||
},
|
||||
"exclude": [
|
||||
"./.meteor/**",
|
||||
"./packages/**",
|
||||
"./_build/**",
|
||||
"./public/_build-bundles/**",
|
||||
"./public/_build-assets/**",
|
||||
"./private/_build-assets/**"
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor');
|
||||
export async function setupMeteorApp(appName) {
|
||||
// Create a unique temporary directory
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 10);
|
||||
const tempDir = path.join(os.tmpdir(), `${appName}-${randomSuffix}`);
|
||||
const tempDir = path.join(os.tmpdir(), `meteortest-${appName}-${randomSuffix}`);
|
||||
|
||||
// Source app directory
|
||||
const sourceAppDir = path.join(__dirname, 'apps', appName);
|
||||
@@ -230,7 +230,7 @@ export async function runMeteorCommand(command, args = [], cwd, options = {}) {
|
||||
export async function createMeteorApp(appName, example, options = {}) {
|
||||
// Create a unique temporary directory that will be the app directory directly
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 10);
|
||||
const tempAppName= `${appName}-${randomSuffix}`;
|
||||
const tempAppName= `meteortest-${appName}-${randomSuffix}`;
|
||||
const tempDir = path.join(os.tmpdir(), tempAppName);
|
||||
|
||||
console.log(`Creating new Meteor app '${appName}' with example '${example}' in ${tempDir}...`);
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import {
|
||||
killProcessByPort,
|
||||
setupMeteorApp,
|
||||
runMeteorApp,
|
||||
cleanupTempDir,
|
||||
killMeteorProcess,
|
||||
createMeteorApp,
|
||||
runMeteorCommand,
|
||||
wait,
|
||||
appendFileContent,
|
||||
waitForMeteorOutput,
|
||||
waitForPlaywrightConsole,
|
||||
runMeteorTests,
|
||||
buildMeteorApp
|
||||
createMeteorApp
|
||||
} from "./helpers";
|
||||
import { assertMeteorReactApp, assertRspackScriptTag, assertFileExist } from './assertions';
|
||||
import { testMeteorBundler, testMeteorRspackBundler } from './test-helpers';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import execa from 'execa';
|
||||
|
||||
describe('React App Bundling /', () => {
|
||||
describe('Meteor Creator /', () => {
|
||||
@@ -69,277 +59,18 @@ describe('React App Bundling /', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meteor Bundler /', () => {
|
||||
const PORT = 3101;
|
||||
let meteorProcess;
|
||||
let tempDir;
|
||||
describe('Meteor Bundler /', testMeteorBundler({
|
||||
appName: 'react',
|
||||
port: 3101
|
||||
}));
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
|
||||
// Setup the Meteor app
|
||||
tempDir = (await setupMeteorApp('react'))?.tempDir;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up the temporary directory
|
||||
await cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
test(`"meteor run" / should start the app`, async () => {
|
||||
// Run the Meteor app
|
||||
meteorProcess = (await runMeteorApp(tempDir, PORT))?.meteorProcess;
|
||||
|
||||
// Assert that the Meteor React app is running correctly
|
||||
await assertMeteorReactApp(PORT);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meteor+Rspack Bundler /', () => {
|
||||
const PORT = 3102;
|
||||
let meteorProcess;
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
await killProcessByPort('8080');
|
||||
|
||||
// Setup the Meteor app
|
||||
tempDir = (await setupMeteorApp('react'))?.tempDir;
|
||||
|
||||
// Add Rspack package
|
||||
await runMeteorCommand('add', ['rspack'], tempDir, { checkExitCode: true });
|
||||
|
||||
// Run the Meteor app to install Rspack
|
||||
const result = await runMeteorApp(tempDir, PORT, {
|
||||
waitForOutput: "=> App running at:",
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(1000);
|
||||
|
||||
// Assert that the config files exists
|
||||
await assertFileExist(tempDir, '.gitignore', { content: '_build' });
|
||||
await assertFileExist(tempDir, 'rspack.config.js', { content: '@meteorjs/rspack' });
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up the temporary directory
|
||||
await cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
test(`"meteor run" / should run and rebuild the app with Rspack`, async () => {
|
||||
// Run the Meteor app and wait for "restarted at" output
|
||||
const result = await runMeteorApp(tempDir, PORT, {
|
||||
waitForOutput: "=> App running at:",
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-meteor.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-meteor.js');
|
||||
|
||||
// Assert that the Meteor React app is running correctly
|
||||
await assertMeteorReactApp(PORT);
|
||||
|
||||
// Assert that the app is using Rspack
|
||||
await assertRspackScriptTag(PORT, true);
|
||||
|
||||
// Update the client code
|
||||
await appendFileContent(tempDir, 'client/main.jsx', {
|
||||
content: 'if (Meteor.isDevelopment) console.log("Hello from dev client");',
|
||||
});
|
||||
await waitForPlaywrightConsole('Hello from dev client');
|
||||
|
||||
// Update the server code
|
||||
await appendFileContent(tempDir, 'server/main.js', {
|
||||
content: 'if (Meteor.isDevelopment) console.log("Hello from dev server");',
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
'Hello from dev server'
|
||||
);
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
test(`"meteor run --production" / should run and rebuild the app with Rspack in production`, async () => {
|
||||
// Run the Meteor app and wait for "restarted at" output
|
||||
const result = await runMeteorApp(tempDir, PORT, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: ['--production'],
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-meteor.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-meteor.js');
|
||||
|
||||
await assertFileExist(tempDir, 'server/main.js');
|
||||
|
||||
// Assert that the Meteor React app is running correctly
|
||||
await assertMeteorReactApp(PORT);
|
||||
|
||||
// Assert that the app is using Rspack
|
||||
await assertRspackScriptTag(PORT, false);
|
||||
|
||||
// Update the client code
|
||||
await appendFileContent(tempDir, 'client/main.jsx', {
|
||||
content: 'if (Meteor.isProduction) console.log("Hello from prod client");',
|
||||
});
|
||||
await waitForPlaywrightConsole('Hello from prod client');
|
||||
|
||||
// Update the server code
|
||||
await appendFileContent(tempDir, 'server/main.js', {
|
||||
content: 'if (Meteor.isProduction) console.log("Hello from prod server");',
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
'Hello from prod server'
|
||||
);
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
test(`"meteor test" / should run tests with Rspack`, async () => {
|
||||
const result = await runMeteorTests(tempDir, PORT, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: [],
|
||||
checkTestResults: false,
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/test/test-entry.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-meteor.js');
|
||||
|
||||
// Update the test code
|
||||
await appendFileContent(tempDir, 'tests/main.js', {
|
||||
content: 'console.log("Hello from test");',
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
'Hello from test'
|
||||
);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
});
|
||||
|
||||
test(`"meteor test --once" / should run tests once with Rspack`, async () => {
|
||||
// Test the app with Rspack once
|
||||
await runMeteorTests(tempDir, PORT, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: ['--once'],
|
||||
checkTestResults: true,
|
||||
});
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/test/test-entry.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-meteor.js');
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(PORT);
|
||||
});
|
||||
|
||||
test(`"meteor build" / should build the app with Rspack`, async () => {
|
||||
// Build the app with Rspack
|
||||
const { buildOutputDir, processResult } = await buildMeteorApp(tempDir, {
|
||||
commandOptions: ['--directory'],
|
||||
captureOutput: true
|
||||
});
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
try {
|
||||
// Assert that the build output directory exists
|
||||
const buildDirExists = await fs.pathExists(buildOutputDir);
|
||||
expect(buildDirExists).toBe(true);
|
||||
|
||||
// Assert that the main.js file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/main.js`)).toBe(true);
|
||||
|
||||
// Assert that the server/package.json file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/package.json`)).toBe(true);
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/program.json`)).toBe(true);
|
||||
|
||||
// Assert that the [web.browser|web.browser.legacy]/program.json file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser/program.json`)).toBe(true);
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser.legacy/program.json`)).toBe(true);
|
||||
|
||||
// Run npm install in the server directory
|
||||
console.log('Running npm install in the server directory...');
|
||||
const serverDir = path.join(buildOutputDir, 'bundle', 'programs', 'server');
|
||||
const npmInstallResult = await execa('npm', ['install'], {
|
||||
cwd: serverDir,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
// Check if the npm install command was successful
|
||||
expect(npmInstallResult.exitCode).toBe(0);
|
||||
} finally {
|
||||
// Clean up the build output directory
|
||||
await cleanupTempDir(buildOutputDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({
|
||||
appName: 'react',
|
||||
port: 3102,
|
||||
filePaths: {
|
||||
client: 'client/main.jsx',
|
||||
server: 'server/main.js',
|
||||
test: 'tests/main.js'
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
375
tools/modern-tests/test-helpers.js
Normal file
375
tools/modern-tests/test-helpers.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* This file contains helper functions for testing Meteor applications.
|
||||
* It provides reusable test patterns for both the test apps.
|
||||
*/
|
||||
|
||||
import {
|
||||
killProcessByPort,
|
||||
setupMeteorApp,
|
||||
runMeteorApp,
|
||||
cleanupTempDir,
|
||||
killMeteorProcess,
|
||||
runMeteorCommand,
|
||||
wait,
|
||||
appendFileContent,
|
||||
waitForMeteorOutput,
|
||||
waitForPlaywrightConsole,
|
||||
runMeteorTests,
|
||||
buildMeteorApp
|
||||
} from "./helpers";
|
||||
import { assertMeteorReactApp, assertRspackScriptTag, assertFileExist } from './assertions';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import execa from 'execa';
|
||||
|
||||
/**
|
||||
* Helper function to set up and run tests for the Meteor Bundler
|
||||
* @param {Object} options - Options for the test
|
||||
* @param {string} options.appName - Name of the app ('react' or 'typescript')
|
||||
* @param {number} options.port - Port to run the app on
|
||||
* @param {Function} options.customAssertions - Custom assertions to run after the app is started
|
||||
* @returns {Function} - Jest test function
|
||||
*/
|
||||
export function testMeteorBundler(options) {
|
||||
const { appName, port, customAssertions } = options;
|
||||
|
||||
return () => {
|
||||
let meteorProcess;
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
|
||||
// Setup the Meteor app
|
||||
tempDir = (await setupMeteorApp(appName))?.tempDir;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up the temporary directory
|
||||
await cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
test(`"meteor run" / should start the app`, async () => {
|
||||
// Run the Meteor app
|
||||
meteorProcess = (await runMeteorApp(tempDir, port))?.meteorProcess;
|
||||
|
||||
// Assert that the Meteor app is running correctly
|
||||
await assertMeteorReactApp(port, { title: appName });
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions) {
|
||||
await customAssertions({ tempDir, port, meteorProcess });
|
||||
}
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set up and run tests for the Meteor+Rspack Bundler
|
||||
* @param {Object} options - Options for the test
|
||||
* @param {string} options.appName - Name of the app ('react', 'typescript', etc)
|
||||
* @param {number} options.port - Port to run the app on
|
||||
* @param {Object} options.filePaths - File paths for the app
|
||||
* @param {string} options.filePaths.client - Client file path (e.g., 'client/main.jsx')
|
||||
* @param {string} options.filePaths.server - Server file path (e.g., 'server/main.js')
|
||||
* @param {string} options.filePaths.test - Test file path (e.g., 'tests/main.js')
|
||||
* @param {Object} options.customMessages - Custom messages for console logs
|
||||
* @param {string} options.customMessages.devClient - Message for development client
|
||||
* @param {string} options.customMessages.devServer - Message for development server
|
||||
* @param {string} options.customMessages.prodClient - Message for production client
|
||||
* @param {string} options.customMessages.prodServer - Message for production server
|
||||
* @param {string} options.customMessages.test - Message for test
|
||||
* @param {Function} options.customAssertions - Custom assertions to run after each test
|
||||
* @returns {Function} - Jest test function
|
||||
*/
|
||||
export function testMeteorRspackBundler(options) {
|
||||
const {
|
||||
appName,
|
||||
port,
|
||||
filePaths = {
|
||||
client: 'client/main.jsx',
|
||||
server: 'server/main.js',
|
||||
test: 'tests/main.js'
|
||||
},
|
||||
customMessages = {
|
||||
devClient: "Hello from dev client",
|
||||
devServer: "Hello from dev server",
|
||||
prodClient: "Hello from prod client",
|
||||
prodServer: "Hello from prod server",
|
||||
test: "Hello from test"
|
||||
},
|
||||
customAssertions
|
||||
} = options;
|
||||
|
||||
return () => {
|
||||
let meteorProcess;
|
||||
let tempDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
await killProcessByPort('8080');
|
||||
|
||||
// Setup the Meteor app
|
||||
tempDir = (await setupMeteorApp(appName))?.tempDir;
|
||||
|
||||
// Add Rspack package
|
||||
await runMeteorCommand('add', ['rspack'], tempDir, { checkExitCode: true });
|
||||
|
||||
// Run the Meteor app to install Rspack
|
||||
const result = await runMeteorApp(tempDir, port, {
|
||||
waitForOutput: "=> App running at:",
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(1000);
|
||||
|
||||
// Assert that the config files exists
|
||||
await assertFileExist(tempDir, '.gitignore', { content: '_build' });
|
||||
await assertFileExist(tempDir, 'rspack.config.js', { content: '@meteorjs/rspack' });
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up the temporary directory
|
||||
await cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
test.only(`"meteor run" / should run and rebuild the app with Rspack`, async () => {
|
||||
// Run the Meteor app and wait for "restarted at" output
|
||||
const result = await runMeteorApp(tempDir, port, {
|
||||
waitForOutput: "=> App running at:",
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/client-meteor.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-dev/server-meteor.js');
|
||||
|
||||
// Assert that the Meteor app is running correctly
|
||||
await assertMeteorReactApp(port, { title: appName });
|
||||
|
||||
// Assert that the app is using Rspack
|
||||
await assertRspackScriptTag(port, true);
|
||||
|
||||
// Update the client code
|
||||
await appendFileContent(tempDir, filePaths.client, {
|
||||
content: `if (Meteor.isDevelopment) console.log("${customMessages.devClient}");`,
|
||||
});
|
||||
await waitForPlaywrightConsole(customMessages.devClient);
|
||||
|
||||
// Update the server code
|
||||
await appendFileContent(tempDir, filePaths.server, {
|
||||
content: `if (Meteor.isDevelopment) console.log("${customMessages.devServer}");`,
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
customMessages.devServer
|
||||
);
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions && customAssertions.afterRun) {
|
||||
await customAssertions.afterRun({ tempDir, port, meteorProcess, result });
|
||||
}
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
test(`"meteor run --production" / should run and rebuild the app with Rspack in production`, async () => {
|
||||
// Run the Meteor app and wait for "restarted at" output
|
||||
const result = await runMeteorApp(tempDir, port, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: ['--production'],
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/client-meteor.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-entry.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/main-prod/server-meteor.js');
|
||||
|
||||
await assertFileExist(tempDir, filePaths.server);
|
||||
|
||||
// Assert that the Meteor app is running correctly
|
||||
await assertMeteorReactApp(port, { title: appName });
|
||||
|
||||
// Assert that the app is using Rspack
|
||||
await assertRspackScriptTag(port, false);
|
||||
|
||||
// Update the client code
|
||||
await appendFileContent(tempDir, filePaths.client, {
|
||||
content: `if (Meteor.isProduction) console.log("${customMessages.prodClient}");`,
|
||||
});
|
||||
await waitForPlaywrightConsole(customMessages.prodClient);
|
||||
|
||||
// Update the server code
|
||||
await appendFileContent(tempDir, filePaths.server, {
|
||||
content: `if (Meteor.isProduction) console.log("${customMessages.prodServer}");`,
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
customMessages.prodServer
|
||||
);
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions && customAssertions.afterRunProduction) {
|
||||
await customAssertions.afterRunProduction({ tempDir, port, meteorProcess, result });
|
||||
}
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
await killProcessByPort('8080');
|
||||
});
|
||||
|
||||
test(`"meteor test" / should run tests with Rspack`, async () => {
|
||||
const result = await runMeteorTests(tempDir, port, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: [],
|
||||
checkTestResults: false,
|
||||
});
|
||||
meteorProcess = result.meteorProcess;
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/test/test-entry.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-meteor.js');
|
||||
|
||||
// Update the test code
|
||||
await appendFileContent(tempDir, filePaths.test, {
|
||||
content: `console.log("${customMessages.test}");`,
|
||||
});
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
customMessages.test
|
||||
);
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions && customAssertions.afterTest) {
|
||||
await customAssertions.afterTest({ tempDir, port, meteorProcess, result });
|
||||
}
|
||||
|
||||
// Kill the meteor process
|
||||
await killMeteorProcess(meteorProcess);
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
});
|
||||
|
||||
test(`"meteor test --once" / should run tests once with Rspack`, async () => {
|
||||
// Test the app with Rspack once
|
||||
const result = await runMeteorTests(tempDir, port, {
|
||||
waitForOutput: "=> App running at:",
|
||||
commandOptions: ['--once'],
|
||||
checkTestResults: true,
|
||||
});
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
// Assert that the app files exists
|
||||
await assertFileExist(tempDir, '_build/test/test-entry.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-rspack.js');
|
||||
await assertFileExist(tempDir, '_build/test/test-meteor.js');
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions && customAssertions.afterTestOnce) {
|
||||
await customAssertions.afterTestOnce({ tempDir, port, result });
|
||||
}
|
||||
|
||||
// Ensure any process on the port is killed
|
||||
await killProcessByPort(port);
|
||||
});
|
||||
|
||||
test(`"meteor build" / should build the app with Rspack`, async () => {
|
||||
// Build the app with Rspack
|
||||
const { buildOutputDir, processResult } = await buildMeteorApp(tempDir, {
|
||||
commandOptions: ['--directory'],
|
||||
captureOutput: true
|
||||
});
|
||||
|
||||
// Wait for a margin
|
||||
await wait(500);
|
||||
|
||||
try {
|
||||
// Assert that the build output directory exists
|
||||
const buildDirExists = await fs.pathExists(buildOutputDir);
|
||||
expect(buildDirExists).toBe(true);
|
||||
|
||||
// Assert that the main.js file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/main.js`)).toBe(true);
|
||||
|
||||
// Assert that the server/package.json file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/package.json`)).toBe(true);
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/program.json`)).toBe(true);
|
||||
|
||||
// Assert that the [web.browser|web.browser.legacy]/program.json file exists
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser/program.json`)).toBe(true);
|
||||
expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser.legacy/program.json`)).toBe(true);
|
||||
|
||||
// Run npm install in the server directory
|
||||
console.log('Running npm install in the server directory...');
|
||||
const serverDir = path.join(buildOutputDir, 'bundle', 'programs', 'server');
|
||||
const npmInstallResult = await execa('npm', ['install'], {
|
||||
cwd: serverDir,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
// Check if the npm install command was successful
|
||||
expect(npmInstallResult.exitCode).toBe(0);
|
||||
|
||||
// Run custom assertions if provided
|
||||
if (customAssertions && customAssertions.afterBuild) {
|
||||
await customAssertions.afterBuild({ tempDir, buildOutputDir, processResult });
|
||||
}
|
||||
} finally {
|
||||
// Clean up the build output directory
|
||||
await cleanupTempDir(buildOutputDir);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
33
tools/modern-tests/typescript.test.js
Normal file
33
tools/modern-tests/typescript.test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
waitForMeteorOutput
|
||||
} from "./helpers";
|
||||
import { testMeteorBundler, testMeteorRspackBundler } from './test-helpers';
|
||||
|
||||
describe('TypeScript App Bundling /', () => {
|
||||
describe('Meteor Bundler /', testMeteorBundler({
|
||||
appName: 'typescript',
|
||||
port: 3201
|
||||
}));
|
||||
|
||||
describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({
|
||||
appName: 'typescript',
|
||||
port: 3202,
|
||||
filePaths: {
|
||||
client: 'client/main.tsx',
|
||||
server: 'server/main.ts',
|
||||
test: 'tests/main.ts'
|
||||
},
|
||||
customAssertions: {
|
||||
afterRun: async ({ result }) => {
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*\[Rspack Client].*No TypeScript errors found\./
|
||||
);
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*\[Rspack Server].*No TypeScript errors found\./
|
||||
);
|
||||
},
|
||||
}
|
||||
}));
|
||||
});
|
||||
Reference in New Issue
Block a user