Skip to content
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
3 changes: 3 additions & 0 deletions .changeset/lazy-beds-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---

---
21 changes: 21 additions & 0 deletions packages/testing-library/kitten-lynx/lynx.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
import { defineConfig } from '@lynx-js/rspeedy';

export default defineConfig({
server: {
host: '0.0.0.0',
port: 3001,
},
source: {
entry: {
'react-example': './test-fixture/cases/react-example/index.tsx',
},
},
plugins: [
pluginReactLynx(),
],
environments: {
lynx: {},
web: {},
},
});
12 changes: 12 additions & 0 deletions packages/testing-library/kitten-lynx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@
],
"scripts": {
"build": "tsc",
"serve": "rspeedy dev",
"test": "vitest run"
},
"dependencies": {
"@lynx-js/devtool-connector": "workspace:*",
"@yume-chan/adb": "catalog:adb",
"@yume-chan/adb-server-node-tcp": "catalog:adb"
},
"devDependencies": {
"@lynx-js/preact-devtools": "^5.0.1",
"@lynx-js/react": "workspace:*",
"@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/rspeedy": "workspace:*",
"@lynx-js/types": "3.7.0",
"@types/react": "^18.3.28",
"execa": "^9.6.1",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
}
}
8 changes: 5 additions & 3 deletions packages/testing-library/kitten-lynx/src/KittenLynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class KittenLynxView {
* @throws An error if it times out waiting for the devtool server to boot (60s limit).
* @throws An error if the specific session for the URL cannot be found (30s limit) or cannot be attached.
*/
async goto(url: string, _options?: unknown): Promise<void> {
async goto(url: string, options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? 30000;
const urlPath = url.split('/').pop() || url;

// Wait until the Lynx app has booted and registered its devtool server.
Expand Down Expand Up @@ -167,7 +168,7 @@ export class KittenLynxView {
let matchedSessionId: number | undefined;
const navStartTime = Date.now();
let pollLoops = 0;
while (Date.now() - navStartTime < 30000) {
while (Date.now() - navStartTime < timeout) {
pollLoops++;
await setTimeout(500);
try {
Expand Down Expand Up @@ -222,7 +223,7 @@ export class KittenLynxView {

if (matchedSessionId === undefined) {
console.error(
`[goto] Failed to find session for URL after 30000ms: ${url}`,
`[goto] Failed to find session for URL after ${timeout}ms: ${url}`,
);
} else {
console.log('matchedSessionId', matchedSessionId);
Expand Down Expand Up @@ -283,6 +284,7 @@ export class KittenLynxView {
this._connector,
);

await channel.send('DOM.enable' as any, {});
const response = await channel.send('DOM.getDocument', {
depth: -1,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
:root {
background-color: #000;
--color-text: #fff;
}

.Background {
position: fixed;
background: radial-gradient(
71.43% 62.3% at 46.43% 36.43%,
rgba(18, 229, 229, 0) 15%,
rgba(239, 155, 255, 0.3) 56.35%,
#ff6448 100%
);
box-shadow: 0px 12.93px 28.74px 0px #ffd28db2 inset;
border-radius: 50%;
width: 200vw;
height: 200vw;
top: -60vw;
left: -14.27vw;
transform: rotate(15.25deg);
}

.App {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

text {
color: var(--color-text);
}

.Banner {
flex: 5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
}

.Logo {
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}

.Logo--react {
width: 100px;
height: 100px;
animation: Logo--spin infinite 20s linear;
}

.Logo--lynx {
width: 100px;
height: 100px;
animation: Logo--shake infinite 0.5s ease;
}

@keyframes Logo--spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@keyframes Logo--shake {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

.Content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.Arrow {
width: 24px;
height: 24px;
}

.Title {
font-size: 36px;
font-weight: 700;
}

.Subtitle {
font-style: italic;
font-size: 22px;
font-weight: 600;
margin-bottom: 8px;
}

.Description {
font-size: 20px;
color: rgba(255, 255, 255, 0.85);
margin: 15rpx;
}

.Hint {
font-size: 12px;
margin: 5px;
color: rgba(255, 255, 255, 0.65);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useCallback, useEffect, useState } from '@lynx-js/react';

import './App.css';
import arrow from './assets/arrow.png';
import lynxLogo from './assets/lynx-logo.png';
import reactLynxLogo from './assets/react-logo.png';
import { useFlappy } from './useFlappy.js';

export function App() {
const [alterLogo, setAlterLogo] = useState(false);
const [logoY, jump] = useFlappy();

useEffect(() => {
console.info('Hello, ReactLynx');
}, []);

const onTap = useCallback(() => {
'background-only';
setAlterLogo(prevAlterLogo => !prevAlterLogo);
}, []);

return (
<view bindtap={jump}>
<view className='Background' />
<view className='App'>
<view className='Banner'>
<view
className='Logo'
style={{ transform: `translateY(${logoY}px)` }}
bindtap={onTap}
>
{alterLogo
? <image src={reactLynxLogo} className='Logo--react' />
: <image src={lynxLogo} className='Logo--lynx' />}
</view>
<text className='Title'>React</text>
<text className='Subtitle'>on Lynx</text>
</view>
<view className='Content'>
<image src={arrow} className='Arrow' />
<text className='Description'>Tap the logo and have fun!</text>
<text className='Hint'>
Edit<text
style={{
fontStyle: 'italic',
color: 'rgba(255, 255, 255, 0.85)',
}}
>
{' cases/react-example/App.tsx '}
</text>
to see updates!
</text>
</view>
<view style={{ flex: 1 }}></view>
</view>
</view>
);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Framework-agnostic flappy-bird physics engine.
*
* Manages gravity, jump impulse, and a 60fps game loop.
* Wire it up to any UI framework by calling `jump()` on tap
* and reading `getY()` in the loop callback.
*/

export interface FlappyOptions {
/** Downward acceleration per frame (default 0.6) */
gravity?: number;
/** Upward impulse per tap — negative value (default -12) */
jumpForce?: number;
/** Impulse stacking factor for rapid taps (default 0.6) */
stackFactor?: number;
/** Frame interval in ms (default 16 ≈ 60fps) */
frameMs?: number;
}

export type OnUpdate = (y: number) => void;

export interface FlappyEngine {
/** Call on each tap to apply upward impulse. */
jump(): void;
/** Current Y offset (0 = ground, negative = airborne). */
getY(): number;
/** Stop the game loop and clean up. */
destroy(): void;
}

export function createFlappy(
onUpdate: OnUpdate,
options: FlappyOptions = {},
): FlappyEngine {
const {
gravity = 0.6,
jumpForce = -12,
stackFactor = 0.6,
frameMs = 16,
} = options;

let y = 0;
let velocity = 0;
let timer: ReturnType<typeof setTimeout> | null = null;

function loop() {
velocity += gravity;
y += velocity;
if (y >= 0) {
y = 0;
velocity = 0;
timer = null;
onUpdate(y);
return;
}
onUpdate(y);
timer = setTimeout(loop, frameMs);
}

function jump() {
// Stack impulse on rapid taps, clamped to one full jumpForce
velocity = Math.max(velocity + jumpForce * stackFactor, jumpForce);
if (!timer) {
loop();
}
}

function destroy() {
if (timer) {
clearTimeout(timer);
timer = null;
}
}

return { jump, getY: () => y, destroy };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import '@lynx-js/preact-devtools';
import '@lynx-js/react/debug';
import { root } from '@lynx-js/react';

import { App } from './App.jsx';

root.render(
<App />,
);

if (import.meta.webpackHot) {
import.meta.webpackHot.accept();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="@lynx-js/rspeedy/client" />
Loading
Loading