Add e2e test (#1656)

* test: add e2e test
This commit is contained in:
luzhuang
2023-12-21 18:03:08 +08:00
committed by GitHub
parent 6405eddb8e
commit 8200bfdd5e
43 changed files with 2488 additions and 40 deletions

3
.gitattributes vendored
View File

@@ -1,3 +1,4 @@
# We'll let Git's auto-detection algorithm infer if a file is text. If it is,
# enforce LF line endings regardless of OS or git configurations.
* text=auto eol=lf
* text=auto eol=lf
e2e/fixtures/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -48,6 +48,8 @@ The following is a set of guidelines for contributing to Galacean. Please spend
- Clone the Galacean playground repository and write a demo for your change.
- Write an uint test in the Galacean repository and run `npm run test` to execute the uint test.
- [Write an e2e test](https://github.com/galacean/runtime/wiki/How-to-write-an-e2e-Test-for-runtime) in the Galacean repository and run `npm run e2e` to execute the e2e test.
### Submitting a Pull Request
@@ -142,4 +144,4 @@ git pull --ff upstream master
## Credits
<br />Thank you to all the people who have already contributed to Galacean!<br />
<br />// WIP: Contributors
<br />// WIP: Contributors

View File

@@ -73,3 +73,53 @@ jobs:
- name: Upload coverage to Codecov
run: ./node_modules/.bin/codecov
- run: curl -s https://codecov.io/bash
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
with:
lfs: true
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install dependencies
run: pnpm i
- name: Build
run: npm run build
- name: Run Cypress Tests
uses: cypress-io/github-action@v5
with:
start: npm run e2e:case
wait-on: 'http://localhost:5175'
wait-on-timeout: 120
browser: chrome
- name: Upload Diff
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-diff
path: e2e/diff/
- name: Upload Origin
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-origin
path: e2e/fixtures/originImage
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: e2e/screenshots/

5
.gitignore vendored
View File

@@ -7,6 +7,7 @@ tmp
/packages/*/types
/packages/*/doc
/tests/node_modules
/e2e/node_modules
/playground/node_modules
types
/packages/*/test/fixtures/**/node_modules
@@ -25,3 +26,7 @@ stats.html
tsconfig.tsbuildinfo
api
yarn.lock
e2e/videos/*
e2e/screenshots/*
e2e/diff/*
e2e/.dev/mpa

3
.husky/post-checkout Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
git lfs post-checkout "$@"

3
.husky/post-commit Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
git lfs post-commit "$@"

3
.husky/post-merge Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
git lfs post-merge "$@"

3
.husky/pre-push Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; }
git lfs pre-push "$@"

View File

@@ -75,6 +75,16 @@ Everyone is welcome to join us! Whether you find a bug, have a great feature req
Make sure to read the [Contributing Guide](.github/HOW_TO_CONTRIBUTE.md) / [贡献指南](https://github.com/galacean/engine/wiki/%E5%A6%82%E4%BD%95%E4%B8%8E%E6%88%91%E4%BB%AC%E5%85%B1%E5%BB%BA-Oasis-%E5%BC%80%E6%BA%90%E4%BA%92%E5%8A%A8%E5%BC%95%E6%93%8E) before submitting changes.
## Clone
Prerequisites:
- [git-lfs](https://git-lfs.com/) (Install by official website)
Clone this repository:
```sh
git clone git@github.com:galacean/runtime.git
```
## Build
Prerequisites:

71
cypress.config.ts Normal file
View File

@@ -0,0 +1,71 @@
import { defineConfig } from "cypress";
import { compare } from "odiff-bin";
const path = require("path");
const fs = require("fs-extra");
const downloadDirectory = path.join(__dirname, "e2e/downloads");
let isRunningInCommandLine = false;
export default defineConfig({
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
baseUrl: "http://localhost:5175",
defaultCommandTimeout: 60000,
fileServerFolder: "e2e",
supportFile: "e2e/support/e2e.ts",
fixturesFolder: "e2e/fixtures",
screenshotsFolder: "e2e/screenshots",
videosFolder: "e2e/videos",
specPattern: "e2e/tests/*.cy.ts",
setupNodeEvents(on, config) {
// implement node event listeners here
on("before:browser:launch", (browser, launchOptions) => {
console.log("launching browser %s is headless? %s", browser.name, browser.isHeadless);
// supply the absolute path to an unpacked extension's folder
// NOTE: extensions cannot be loaded in headless Chrome
if (fs.existsSync("e2e/diff")) {
fs.rmdirSync("e2e/diff", { recursive: true });
}
if (browser.name === "chrome") {
launchOptions.preferences.default["download"] = {
default_directory: downloadDirectory
};
}
if (browser.isHeadless) {
isRunningInCommandLine = true;
}
launchOptions.args.push("--force-device-scale-factor=1");
return launchOptions;
}),
on("task", {
async compare({ fileName, options }) {
fileName += ".png";
const baseFolder = "e2e/fixtures/originImage/";
const newFolder = path.join("e2e/screenshots", isRunningInCommandLine ? options.specFolder : "");
const diffFolder = path.join("e2e/diff", options.specFolder);
if (!fs.existsSync(diffFolder)) {
fs.mkdirSync(diffFolder, { recursive: true });
}
const baseImage = path.join(baseFolder, fileName);
const newImage = path.join(newFolder, fileName);
const diffImage = path.join(diffFolder, fileName);
console.log("comparing base image %s to the new image %s", baseImage, newImage);
if (options) {
console.log("odiff options %o", options);
}
const started = +new Date();
const result = await compare(baseImage, newImage, diffImage, options);
const finished = +new Date();
const elapsed = finished - started;
console.log("odiff took %dms", elapsed);
console.log(result);
return result;
}
});
}
},
chromeWebSecurity: false
});

BIN
e2e/.dev/AlibabaSans.ttf Normal file

Binary file not shown.

39
e2e/.dev/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Galacean Case</title>
</head>
<body>
<div class="container">
<div class="header">
<a class="logo" href="https://galacean.antgroup.com/" target="Home">
<img
src="https://mdn.alipayobjects.com/huamei_qbugvr/afts/img/A*ppQsSphM7uUAAAAAAAAAAAAADtKFAQ/original"
alt=""
/>
<span> Galacean </span>
</a>
</div>
<input
placeholder="search..."
class="search-bar"
id="searchBar"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<div class="nav-left">
<div class="item-list" id="itemList"></div>
</div>
<div class="nav-right">
<iframe id="iframe" allowfullscreen src="" frameborder="0"></iframe>
</div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>

76
e2e/.dev/index.js Normal file
View File

@@ -0,0 +1,76 @@
import './index.sass';
import demoList from './mpa/.demoList.json';
const itemListDOM = document.getElementById('itemList');
const searchBarDOM = document.getElementById('searchBar');
const iframe = document.getElementById('iframe');
const items = []; // itemDOM,label
Object.keys(demoList).forEach((group) => {
const demos = demoList[group];
const groupDOM = document.createElement('div');
const demosDOM = document.createElement('ul');
itemListDOM.appendChild(groupDOM);
groupDOM.appendChild(demosDOM);
demos.forEach((item) => {
const { label, src } = item;
const itemDOM = document.createElement('a');
itemDOM.innerHTML = src;
itemDOM.title = `${src}`;
itemDOM.onclick = function () {
clickItem(itemDOM);
};
demosDOM.appendChild(itemDOM);
items.push({
itemDOM,
label,
src,
});
});
});
searchBarDOM.oninput = () => {
updateFilter(searchBar.value);
};
function updateFilter(value) {
const reg = new RegExp(value, 'i');
items.forEach(({ itemDOM, label, src }) => {
reg.lastIndex = 0;
if (reg.test(label) || reg.test(src)) {
itemDOM.classList.remove('hide');
} else {
itemDOM.classList.add('hide');
}
});
}
function clickItem(itemDOM) {
window.location.hash = `#mpa/${itemDOM.title}`;
}
function onHashChange() {
const hashPath = window.location.hash.split('#')[1];
if (!hashPath) {
clickItem(items[0].itemDOM);
return;
}
iframe.src = hashPath + '.html';
items.forEach(({ itemDOM }) => {
const itemPath = `mpa/${itemDOM.title}`;
if (itemPath === hashPath) {
itemDOM.classList.add('active');
} else {
itemDOM.classList.remove('active');
}
});
}
window.onhashchange = onHashChange;
// init
onHashChange();

97
e2e/.dev/index.sass Normal file
View File

@@ -0,0 +1,97 @@
$header-height: 3.5rem
$header-padding: 1rem
$container-gap: 2rem
$transition-duration: 0.5s
*
box-sizing: border-box
html, body
margin: 0
width: 100%
height: 100%
overflow: hidden
body
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif
font-size: 16px
a
text-decoration: none
color: inherit
display: inline-block
.container
display: flex
height: 100%
user-select: none
.header
position: absolute
top: 0
left: 0
height: $header-height
padding: $header-padding $container-gap
.logo
height: $header-height - 2 * $header-padding
display: flex
align-items: center
img
height: 100%
span
font-weight: bold
padding-left: 10px
font-size: 1.2em
.search-bar
position: absolute
top: $header-height
left: 1rem
padding: $header-padding $container-gap
background: #fff url("//gw-office.alipayobjects.com/basement_prod/225a73b9-1281-4388-9555-1e671899bc25.svg") .6rem .55rem no-repeat
cursor: text
width: 10rem
height: 2rem
color: #666
display: inline-block
border: 1px solid #ccc
border-radius: 2rem
font-size: .9rem
line-height: 2rem
outline: none
.nav-left
display: flex
flex-direction: column
overflow: auto
padding: 1rem $container-gap
margin-top: $header-height + 2rem
.item-list
.hide
height: 0
ul
padding-left: 20px
margin: 0
.title
font-weight: bold
cursor: default
font-size: 1.2em
a
height: 2rem
line-height: 2rem
cursor: pointer
transition-property: color,height
transition-duration: $transition-duration
overflow: hidden
display: block
&.active
text-decoration: underline
color: #096dd9
&:hover
color: #096dd9
.nav-right
flex: 1
box-shadow: rgb(125 121 121) -2px 0px 8px
iframe
width: 100%
height: 100%

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<style>
::-webkit-scrollbar {
width: 0px; /* For vertical scrollbars */
height: 0px; /* For horizontal scrollbars */
}
@font-face {
font-family: "AlibabaSans";
src: url("../AlibabaSans.ttf");
}
html,
body,
canvas {
width: 1200px;
height: 800px;
padding: 0;
margin: 0;
overflow: hidden;
background: black;
border: none;
}
.dg.main > ul {
overflow: initial;
}
div {
font-family: "AlibabaSans";
font-size: 20px;
color: white;
position: absolute;
top: 0;
left: 0;
z-index: 100;
}
</style>
<link rel="preload" href="../AlibabaSans.ttf" as="font" type="font/ttf" crossorigin="anonymous">
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module" src="<%= url %>"></script>
<script>
document.oncontextmenu = (e) => {
e.preventDefault();
};
</script>
</body>
</html>

76
e2e/.dev/vite.config.js Normal file
View File

@@ -0,0 +1,76 @@
const path = require("path");
const fs = require("fs-extra");
const OUT_PATH = "mpa";
const templateStr = fs.readFileSync(path.join(__dirname, "template/iframe.ejs"), "utf8");
// 替换 ejs 模版格式的字符串,如 <%= title %>: templateStr.replaceEJS("title","replaced title");
String.prototype.replaceEJS = function (regStr, replaceStr) {
return this.replace(new RegExp(`<%=\\s*${regStr}\\s*%>`, "g"), replaceStr);
};
// clear mpa
fs.emptyDirSync(path.resolve(__dirname, OUT_PATH));
// create mpa
const demoList = fs
.readdirSync(path.join(__dirname, "../case"))
.filter((name) => /.ts$/.test(name) && name.indexOf(".") !== 0)
.map((name) => {
return {
file: name.split(".ts")[0]
};
});
demoList.forEach(({ file }) => {
const ejs = templateStr.replaceEJS("url", `./${file}.ts`);
fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".ts"), `import "../../case/${file}"`);
fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".html"), ejs);
});
// output demolist
const demoSorted = {};
demoList.forEach(({ file }) => {
if (!demoSorted[file]) {
demoSorted[file] = [];
}
demoSorted[file].push({
src: file
});
});
fs.outputJSONSync(path.join(__dirname, OUT_PATH, ".demoList.json"), demoSorted);
module.exports = {
server: {
open: true,
host: "0.0.0.0",
port: 5175
},
resolve: {
dedupe: ["@galacean/engine"]
},
optimizeDeps: {
exclude: [
"@galacean/engine",
"@galacean/engine-draco",
"@galacean/engine-lottie",
"@galacean/engine-spine",
"@galacean/tools-baker",
"@galacean/engine-toolkit",
"@galacean/engine-toolkit-auxiliary-lines",
"@galacean/engine-toolkit-controls",
"@galacean/engine-toolkit-framebuffer-picker",
"@galacean/engine-toolkit-gizmo",
"@galacean/engine-toolkit-lines",
"@galacean/engine-toolkit-outline",
"@galacean/engine-toolkit-planar-shadow-material",
"@galacean/engine-toolkit-skeleton-viewer",
"@galacean/engine-toolkit-grid-material",
"@galacean/engine-toolkit-navigation-gizmo",
"@galacean/engine-toolkit-geometry-sketch",
"@galacean/engine-toolkit-stats",
"@galacean/engine-toolkit-input-logger"
]
}
};

20
e2e/case/.mockForE2E.ts Normal file
View File

@@ -0,0 +1,20 @@
export const updateForE2E = (engine, deltaTime = 100) => {
engine._vSyncCount = Infinity;
engine._time._lastSystemTime = 0;
let times = 0;
performance.now = function () {
times++;
return times * deltaTime;
};
for (let i = 0; i < 10; ++i) {
engine.update();
}
};
export const e2eReady = () => {
setTimeout(() => {
const text = document.createElement("div");
text.className = "cypressReady";
document.body.appendChild(text);
}, 1000);
}

View File

@@ -0,0 +1,68 @@
/**
* @title Animation Additive
* @category Animation
*/
import {
Animator,
AnimatorControllerLayer,
AnimatorLayerBlendingMode,
AnimatorStateMachine,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
const animator = defaultSceneRoot.getComponent(Animator);
const { animatorController } = animator;
const animatorStateMachine = new AnimatorStateMachine();
const additiveLayer = new AnimatorControllerLayer("additiveLayer");
additiveLayer.stateMachine = animatorStateMachine;
additiveLayer.blendingMode = AnimatorLayerBlendingMode.Additive;
animatorController.addLayer(additiveLayer);
const additivePoseNames = animations.filter((clip) => clip.name.includes("pose")).map((clip) => clip.name);
additivePoseNames.forEach((name) => {
const clip = animator.findAnimatorState(name).clip;
const newState = animatorStateMachine.addState(name);
newState.clipStartTime = 1;
newState.clip = clip;
});
rootEntity.addChild(defaultSceneRoot);
animator.play("walk", 0);
animator.play("sad_pose", 1);
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,49 @@
/**
* @title Animation BlendShape
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
Camera,
DirectLight,
Logger,
SkinnedMeshRenderer,
SystemInfo,
Vector3,
WebGLEngine,
GLTFResource
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 1.0;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(-45, -135, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/746da3e3-fdc9-4155-8fee-0e2a97de4e72.glb")
.then((asset) => {
const { defaultSceneRoot } = asset;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const skinMeshRenderer = defaultSceneRoot.getComponent(SkinnedMeshRenderer);
skinMeshRenderer.blendShapeWeights[0] = 1.0;
animator.play("TheWave");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,47 @@
/**
* @title Animation Play
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
Camera,
DirectLight,
Logger,
SystemInfo,
Vector3,
WebGLEngine,
GLTFResource
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
animator.play("agree");
updateForE2E(engine, 30);
animator.crossFade("walk", 0.5, 0, 0);
updateForE2E(engine, 30);
e2eReady();
});
});

View File

@@ -0,0 +1,188 @@
/**
* @title Animation CustomAnimationClip
* @category Animation
*/
import {
AnimationClip,
AnimationColorCurve,
AnimationFloatCurve,
AnimationVector3Curve,
Animator,
AnimatorController,
AnimatorControllerLayer,
AnimatorStateMachine,
Camera,
Color,
DirectLight,
GLTFResource,
Keyframe,
Logger,
SpotLight,
SystemInfo,
Transform,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraWrap = rootEntity.createChild("camera_wrap");
const cameraEntity = cameraWrap.createChild("camera");
cameraEntity.transform.position = new Vector3(0, 3, 8);
cameraEntity.transform.rotation = new Vector3(-2, 0, 0);
cameraEntity.addComponent(Camera);
const lightWrap = rootEntity.createChild("light_wrap");
const directLightEntity = lightWrap.createChild("light_node");
const directLight = directLightEntity.addComponent(DirectLight);
directLight.intensity = 0.6;
directLightEntity.transform.lookAt(new Vector3(0, 0, 1));
directLightEntity.transform.rotate(new Vector3(0, 90, 0));
const spotLightEntity = lightWrap.createChild("spotLight1");
const spotLightEntity2 = spotLightEntity.clone();
spotLightEntity2.name = "spotLight2";
spotLightEntity2.transform.setRotation(-120, 0, 0);
lightWrap.addChild(spotLightEntity2);
const spotLight = spotLightEntity.addComponent(SpotLight);
spotLight.angle = Math.PI / 60;
spotLightEntity.transform.setPosition(0, 8, 0);
spotLightEntity.transform.setRotation(-60, 0, 0);
const spotLight2 = spotLightEntity2.addComponent(SpotLight);
spotLight2.angle = Math.PI / 60;
spotLightEntity2.transform.setPosition(0, 8, 0);
spotLightEntity2.transform.setRotation(-60, 0, 0);
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/OasisHub/244228a7-361c-4c63-a790-dd9e19d12e78/data.gltf")
.then((gltfResource) => {
const { defaultSceneRoot, animations = [] } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const sceneAnimator = rootEntity.addComponent(Animator);
sceneAnimator.animatorController = new AnimatorController();
const layer = new AnimatorControllerLayer("base");
sceneAnimator.animatorController.addLayer(layer);
const stateMachine = (layer.stateMachine = new AnimatorStateMachine());
const sceneState = stateMachine.addState("sceneAnim");
const sceneClip = (sceneState.clip = new AnimationClip("sceneAnim"));
//custom rotate curve
const rotateCurve = new AnimationVector3Curve();
const key1 = new Keyframe<Vector3>();
key1.time = 0;
key1.value = new Vector3(0, 0, 0);
const key2 = new Keyframe<Vector3>();
key2.time = 15;
key2.value = new Vector3(0, 360, 0);
rotateCurve.addKey(key1);
rotateCurve.addKey(key2);
//custom color curve
const colorCurve = new AnimationColorCurve();
const key3 = new Keyframe<Color>();
key3.time = 0;
key3.value = new Color(1, 0, 0, 1);
const key4 = new Keyframe<Color>();
key4.time = 5;
key4.value = new Color(0, 1, 0, 1);
const key5 = new Keyframe<Color>();
key5.time = 10;
key5.value = new Color(0, 0, 1, 1);
const key6 = new Keyframe<Color>();
key6.time = 15;
key6.value = new Color(1, 0, 0, 1);
colorCurve.addKey(key3);
colorCurve.addKey(key4);
colorCurve.addKey(key5);
colorCurve.addKey(key6);
const color2Curve = new AnimationColorCurve();
const key16 = new Keyframe<Color>();
key16.time = 0;
key16.value = new Color(0, 0, 1, 1);
const key17 = new Keyframe<Color>();
key17.time = 5;
key17.value = new Color(0, 1, 0, 1);
const key18 = new Keyframe<Color>();
key18.time = 10;
key18.value = new Color(1, 0, 0, 1);
const key19 = new Keyframe<Color>();
key19.time = 15;
key19.value = new Color(0, 0, 1, 1);
color2Curve.addKey(key16);
color2Curve.addKey(key17);
color2Curve.addKey(key18);
color2Curve.addKey(key19);
//custom fov curve
const fovCurve = new AnimationFloatCurve();
const key7 = new Keyframe<number>();
key7.time = 0;
key7.value = 45;
const key8 = new Keyframe<number>();
key8.time = 8;
key8.value = 80;
const key9 = new Keyframe<number>();
key9.time = 15;
key9.value = 45;
fovCurve.addKey(key7);
fovCurve.addKey(key8);
fovCurve.addKey(key9);
//custom spotLight1 rotate curve
const spotLight1RotateCurve = new AnimationVector3Curve();
const key10 = new Keyframe<Vector3>();
key10.time = 0;
key10.value = new Vector3(-60, 0, 0);
const key11 = new Keyframe<Vector3>();
key11.time = 10;
key11.value = new Vector3(-120, 0, 0);
const key12 = new Keyframe<Vector3>();
key12.time = 15;
key12.value = new Vector3(-60, 0, 0);
spotLight1RotateCurve.addKey(key10);
spotLight1RotateCurve.addKey(key11);
spotLight1RotateCurve.addKey(key12);
//custom spotLight2 rotate curve
const spotLight2RotateCurve = new AnimationVector3Curve();
const key13 = new Keyframe<Vector3>();
key13.time = 0;
key13.value = new Vector3(-120, 0, 0);
const key14 = new Keyframe<Vector3>();
key14.time = 10;
key14.value = new Vector3(-60, 0, 0);
const key15 = new Keyframe<Vector3>();
key15.time = 15;
key15.value = new Vector3(-120, 0, 0);
spotLight2RotateCurve.addKey(key13);
spotLight2RotateCurve.addKey(key14);
spotLight2RotateCurve.addKey(key15);
sceneClip.addCurveBinding("/light_wrap/spotLight1", SpotLight, "color", colorCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight1", Transform, "rotation", spotLight1RotateCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight2", Transform, "rotation", spotLight2RotateCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight2", SpotLight, "color", color2Curve);
sceneClip.addCurveBinding("/light_wrap", Transform, "rotation", rotateCurve);
// curve can be reused
sceneClip.addCurveBinding("/camera_wrap", Transform, "rotation", rotateCurve);
sceneClip.addCurveBinding("/camera_wrap/camera", Camera, "fieldOfView", fovCurve);
sceneAnimator.play("sceneAnim", 0);
animator.play(animations[0].name, 0);
updateForE2E(engine, 500);
e2eReady();
});
});

View File

@@ -0,0 +1,98 @@
/**
* @title Animation CustomBlendShape
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
AnimationClip,
AnimationFloatArrayCurve,
Animator,
AnimatorController,
AnimatorControllerLayer,
AnimatorStateMachine,
BlendShape,
Camera,
Keyframe,
Logger,
ModelMesh,
SkinnedMeshRenderer,
SystemInfo,
UnlitMaterial,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("cameraNode");
cameraEntity.transform.position = new Vector3(0, 0, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl);
const meshEntity = rootEntity.createChild("meshEntity");
const skinnedMeshRenderer = meshEntity.addComponent(SkinnedMeshRenderer);
const modelMesh = new ModelMesh(engine);
// Set vertices data.
const positions = [
new Vector3(-1.0, -1.0, 1.0),
new Vector3(1.0, -1.0, 1.0),
new Vector3(1.0, 1.0, 1.0),
new Vector3(1.0, 1.0, 1.0),
new Vector3(-1.0, 1.0, 1.0),
new Vector3(-1.0, -1.0, 1.0)
];
modelMesh.setPositions(positions);
// Add SubMesh.
modelMesh.addSubMesh(0, 6);
// Add BlendShape.
const deltaPositions = [
new Vector3(0.0, 0.0, 0.0),
new Vector3(0.0, 0.0, 0.0),
new Vector3(-1.0, 0.0, 0.0),
new Vector3(-1.0, 0.0, 0.0),
new Vector3(1.0, 0.0, 0.0),
new Vector3(0.0, 0.0, 0.0)
];
const blendShape = new BlendShape("BlendShapeA");
blendShape.addFrame(1.0, deltaPositions);
modelMesh.addBlendShape(blendShape);
skinnedMeshRenderer.mesh = modelMesh;
skinnedMeshRenderer.setMaterial(new UnlitMaterial(engine));
// Upload data.
modelMesh.uploadData(false);
const animator = meshEntity.addComponent(Animator);
animator.animatorController = new AnimatorController();
const layer = new AnimatorControllerLayer("base");
animator.animatorController.addLayer(layer);
const stateMachine = (layer.stateMachine = new AnimatorStateMachine());
const state = stateMachine.addState("blendShape");
const clip = (state.clip = new AnimationClip("blendShape"));
//custom blendShape curve
const blendShapeCurve = new AnimationFloatArrayCurve();
const key1 = new Keyframe<Float32Array>();
key1.time = 0;
key1.value = new Float32Array([0]);
const key2 = new Keyframe<Float32Array>();
key2.time = 5;
key2.value = new Float32Array([1]);
blendShapeCurve.addKey(key1);
blendShapeCurve.addKey(key2);
clip.addCurveBinding("", SkinnedMeshRenderer, "blendShapeWeights", blendShapeCurve);
animator.play("blendShape");
updateForE2E(engine, 1000);
e2eReady();
});

View File

@@ -0,0 +1,83 @@
/**
* @title Animation Event
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import * as dat from "dat.gui";
import {
AnimationEvent,
Animator,
Camera,
DirectLight,
Font,
FontStyle,
GLTFResource,
Script,
SystemInfo,
TextRenderer,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
const engine = await WebGLEngine.create({ canvas: "canvas" });
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
console.log(99, SystemInfo.devicePixelRatio);
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
// initText
const textEntity = rootEntity.createChild("text");
const textRenderer = textEntity.addComponent(TextRenderer);
textEntity.transform.setPosition(0, 2, 0);
textRenderer.fontSize = 12;
textRenderer.font = Font.createFromOS(engine, "AlibabaSans");
textRenderer.text = "";
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { defaultSceneRoot, animations } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const state = animator.findAnimatorState("walk");
const clip = state.clip;
const event0 = new AnimationEvent();
event0.functionName = "event0";
event0.time = 0.5;
clip.addEvent(event0);
const event1 = new AnimationEvent();
event1.functionName = "event1";
event1.time = clip.length;
clip.addEvent(event1);
defaultSceneRoot.addComponent(
class extends Script {
event0(): void {
textRenderer.text = "0";
}
event1(): void {
textRenderer.text = "1";
}
}
);
animator.play("walk", 0);
updateForE2E(engine, 500);
e2eReady();
});

46
e2e/case/animator-play.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* @title Animation Play
* @category Animation
*/
import {
Animator,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { updateForE2E, e2eReady } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
animator.play("agree");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,69 @@
/**
* @title Animation Reuse
* @category Animation
*/
import {
Animator,
AssetPromise,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
const promises: AssetPromise<GLTFResource>[] = [];
// origin model
promises.push(
engine.resourceManager.load<GLTFResource>(
"https://gw.alipayobjects.com/os/OasisHub/6f5b1918-1380-4641-a57a-7507503a524c/data.gltf"
)
);
// animation
promises.push(
engine.resourceManager.load<GLTFResource>(
"https://gw.alipayobjects.com/os/OasisHub/9ef53086-67d4-4be6-bff8-449a8074a5bd/data.gltf"
)
);
Promise.all(promises).then((resArr) => {
const modelGLTF = resArr[0];
const animationGLTF = resArr[1];
const { animations: originAnimations = [] } = modelGLTF;
const { animations = [] } = animationGLTF;
const { defaultSceneRoot } = modelGLTF;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const danceState = animator.animatorController.layers[0].stateMachine.addState("dance");
danceState.clip = animations[0];
animator.play("dance");
const animationNames = originAnimations.map((clip) => clip.name);
animationNames.push("dance");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,82 @@
/**
* @title AnimatorStateScript
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
AnimatorState,
Camera,
DirectLight,
Font,
FontStyle,
GLTFResource,
Logger,
StateMachineScript,
SystemInfo,
TextRenderer,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
// initText
const textEntity = rootEntity.createChild("text");
const textRenderer = textEntity.addComponent(TextRenderer);
textEntity.transform.setPosition(0, 2, 0);
textRenderer.fontSize = 12;
textRenderer.font = Font.createFromOS(engine, "AlibabaSans");
textRenderer.text = "";
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const state = animator.findAnimatorState("walk");
state.addStateMachineScript(
class extends StateMachineScript {
onStateEnter(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
textRenderer.text = "0";
console.log("onStateEnter: ", animatorState);
}
onStateUpdate(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
console.log("onStateUpdate: ", animatorState);
}
onStateExit(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
textRenderer.text = "1";
console.log("onStateExit: ", animatorState);
}
}
);
animator.play("walk");
updateForE2E(engine, 30);
animator.crossFade("run", 0.5, 0, 0);
updateForE2E(engine, 100);
e2eReady();
});
});

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9f55ccf7d9ba3eeae64d80dec8a0f1d07a28708b6864200402611c5d70fe2f3
size 31060

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6247265cc5a322be8c5870243db59fb945651e56f046a390991130aa20171a8e
size 72841

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c053b0cc1ac48e4e764d5c16b0ca2d46b5f72e4d07b74ff3b779974af22f40a0
size 30566

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b2f8c4351d39e95152af5604858512e94157b58e74800ec5be8cc9c0344cabe
size 14836

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f816564ea3b61a8f6ec6bb6ab55c4526278f15c3167493bd6e7ae7a8d96b9538
size 9894

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46d72de6ff304523602e6d651bd19de111d8a92c9ecac5854fcd8ae3686262f8
size 30210

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2ae9948fa4082b03e9a2b6e847b2e301497045a1e8a51aed55238dc67fcbb31
size 31219

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a7e5ed546549e84343f7a590eb7d01eed5a8716833b25e305d8356ab174aee6
size 28309

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92beef2371cf9b1cb897902ef47c58646b30133d8ebd418c39820931d226a8f0
size 28203

25
e2e/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@galacean/engine-e2e",
"private": true,
"version": "1.0.0-alpha.6",
"license": "MIT",
"scripts": {
"case": "vite serve .dev --config .dev/vite.config.js",
"b:types": "echo hi"
},
"files": [
],
"dependencies": {
"@galacean/engine-toolkit": "^1.0.0-beta.1",
"@galacean/engine": "workspace:*",
"@galacean/engine-core": "workspace:*",
"@galacean/engine-loader": "workspace:*",
"@galacean/engine-design": "workspace:*",
"@galacean/engine-math": "workspace:*",
"@galacean/engine-rhi-webgl": "workspace:*",
"@galacean/engine-physics-lite": "workspace:*",
"dat.gui": "^0.7.9",
"vite": "^3.1.6",
"sass": "^1.55.0"
}
}

67
e2e/support/commands.ts Normal file
View File

@@ -0,0 +1,67 @@
import { recurse } from "cypress-recurse";
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare global {
namespace Cypress {
interface Chainable {
screenshotWithThreshold(category: string, name: string, threshold?: number): Chainable<Element>;
}
}
}
Cypress.Commands.add("screenshotWithThreshold", (category, name, threshold = 0) => {
cy.visit(`/mpa/${name}.html`);
cy.get(".cypressReady").then(() => {
return new Promise((resolve) => {
const imageName = `${category}_${name}`;
resolve(
recurse(
() => {
return cy
.get("#canvas")
.screenshot(imageName, { overwrite: true, capture: "viewport" })
.then(() => {
return cy.task("compare", {
fileName: imageName,
options: {
specFolder: Cypress.spec.name,
threshold
}
});
});
},
({ match }) => match,
{
limit: 3
}
)
);
});
});
});

20
e2e/support/e2e.ts Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

37
e2e/tests/animator.cy.ts Normal file
View File

@@ -0,0 +1,37 @@
describe("Animator", () => {
it("Animator Play", () => {
cy.screenshotWithThreshold("Animator", "animator-play", 0.3);
});
it("Animator Crossfade", () => {
cy.screenshotWithThreshold("Animator", "animator-crossfade", 0.3);
});
it("Animation Additive", () => {
cy.screenshotWithThreshold("Animator", "animator-additive", 0.3);
});
it("Animator Reuse", () => {
cy.screenshotWithThreshold("Animator", "animator-reuse", 0.3);
});
it("Animation BlendShape", () => {
cy.screenshotWithThreshold("Animator", "animator-blendShape", 0.3);
});
it("Animator CustomBlendShape", () => {
cy.screenshotWithThreshold("Animator", "animator-customBlendShape", 0.3);
});
it("Animator stateMachineScript", () => {
cy.screenshotWithThreshold("Animator", "animator-stateMachineScript", 0.38);
});
it("Animator event", () => {
cy.screenshotWithThreshold("Animator", "animator-event", 0.38);
});
it("Animator CustomAnimationClip", () => {
cy.screenshotWithThreshold("Animator", "animator-customAnimationClip", 0.3);
});
});

8
e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

View File

@@ -18,6 +18,9 @@
"b:miniprogram": "cross-env BUILD_TYPE=MINI rollup -c",
"b:all": "npm run b:types && cross-env BUILD_TYPE=ALL rollup -c",
"clean": "pnpm -r exec rm -rf dist && pnpm -r exec rm -rf types",
"e2e:case": "pnpm -C ./e2e run case",
"e2e": "cypress run --browser chrome --headless",
"e2e:debug": "cypress open",
"prepare": "husky install"
},
"devDependencies": {
@@ -53,7 +56,11 @@
"rollup-plugin-swc3": "^0.10.1",
"ts-node": "^10",
"typescript": "^5.1.6",
"husky": "^8.0.0"
"husky": "^8.0.0",
"fs-extra": "^10.1.0",
"cypress": "^12.17.1",
"cypress-recurse": "^1.23.0",
"odiff-bin": "^2.5.0"
},
"lint-staged": {
"*.{ts}": [

1084
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,5 @@ packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'tests'
- 'tests'
- 'e2e'