@blotch/angular-lynx-测试-图书馆
测试公用事业 角线 apps. Renders Angular 独立组件通过 Lynx PAPI 输入一棵 JSDOM 树,然后通过 @ testing-library/dom (英语) 查询,也可以读取 Lynx 的 accessibility-* 属性.
安装
ng generate @blotch/angular-lynx:add-testing
它安装了这个软件包(和Vitest和jsdom,如果项目没有它们; ng new 和确保 ng test 运行通过AngularLynx的单元测试构建器. 用手安装它: npm install --save-dev @blotch/angular-lynx-testing-library。 。 。 .
设置
无。 测试运行于 ng test建造者@blotch/angular-lynx-build:unit-test,哪个 ng add 设置)在测试前加载此包的设置,只要项目有它. 没有 Vitest 配置或设置文件可以写入 .
设置:
- 启用 Angular JIT 汇编(
@angular/compiler),用于运行时编译的组件 - 安装
LynxTestingEnv在 jsdom 窗口上 - 切换到 Lynx 主线程( PAPI 函数所在)
- Polyfills PAPI 功能测试环境不执行(
__GetParent, (中文(简体) ),__QuerySelector页:1 - 制造
lynx.reportError就像Lynx那样 回去警告你 而不是扔出去 所以@boundary以默认错误处理器显示其倒置 - 安装
@testing-library/jest-dom匹配点
ng test 像应用程序一样,提前编译测试,严格检查模板,就像Angular Language Service所做的:使用 Lynx 元素导入的组件 LYNX_ELEMENTS 从 @blotch/angular-lynx。下面的例子.
没有角CLI
在纯 Vitest 下, 自动加载设置, 并定义 AngularLynx 运行时读取的编译时的标记( rsbuilt 插件在应用程序中定义它们, 以及 ng test 设置它们:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
define: {
__DEV__: JSON.stringify(true),
__MAIN_THREAD__: JSON.stringify(true),
__WEB__: JSON.stringify(false),
__PROFILE__: JSON.stringify(false),
__ENABLE_SSR__: JSON.stringify(false),
},
// Angular's JIT compiler reads the metadata of legacy decorators.
oxc: { decorator: { legacy: true } },
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['@blotch/angular-lynx-testing-library/setup'],
},
});
API 密码
render(component, options?)
Bootstrap 独立角构件并返回 JSDOM 容器 @testing-library/dom 与它相连的询问.
import { render } from '@blotch/angular-lynx-testing-library';
const { container, getByText, queryByRole } = await render(MyComponent);
render 实值 自动同步 - 这个 await角质 createApplication,然后把组件绑住。 初始变化检测周期在承诺解决之前运行,因此容器可以立即查询.
选项 :
| 选项 | 类型 | 说明 |
|---|---|---|
providers | (Provider | EnvironmentProviders)[] | 更多供应商注入应用程序(服务、信使、模拟). |
queries | Queries | 自定义要绑定到 container图书馆旁边. |
inputs | RenderInputs<C> | 元件输入的值,按出名键为模板绑定(一个别名输入的别名). 设定在第一次更改检测之前 . |
const { getByText, rerender } = await render(Greeting, {
inputs: { name: 'Lynx' },
});
// Keeps the component and changes only the inputs passed.
await rerender({ inputs: { name: 'Angular' } });
rerender({ inputs }) 只能更改输入 render() 设定: 角置入 inputBinding(),而带有输入约束的组件拒绝其他输入(和 componentRef.setInput()) (中文(简体) ). 给 render() 输入的起始值.
provideZonelessChangeDetection() 财务报告和审定财务报表 provideRenderer() ——你不需要提供.
返回值:
| 财产 | 类型 | 说明 |
|---|---|---|
container | Element | 司法事务司 <page> 根元素。 所有的孩子都是后代. |
unmount | () => void | 摧毁Angular应用程序. |
rerender | ({ inputs }) => Promise<RenderResult> 或者说 (component, options?) => Promise<RenderResult> | 变化投入 render() 设置,保留组件;或使另一个组件处于其位置. |
...queries | —— 说 | 每个查询在 queries 有义务 container (单位:千美元)getByText, (中文(简体) ), getByRole, (中文(简体) ), getByAccessibilityLabel(见a/c.5/49/70,附件). |
cleanup()
摧毁当前 Angular app并重置 Lynx 环境 。 每次测试后自动调用——你只需要这个手动控制.
import { cleanup } from '@blotch/angular-lynx-testing-library';
afterEach(cleanup);
设定 ATL_SKIP_AUTO_CLEANUP=true 以禁用自动清理.
waitForUpdate()
等Angular的无区变化检测调度器冲出待反应更新,再强制同步CD循环.
import { waitForUpdate } from '@blotch/angular-lynx-testing-library';
const count = signal(0);
count.set(1);
await waitForUpdate();
// DOM now reflects count = 1
在事件处理器外发生任何信号突变后调用此功能( 例如: direct .set() 在测试代码中).
下 vi.useFakeTimers()改变探测器 等待假钟, 所以没有什么能使真正的钟. waitForUpdate() 等待一个零延迟 setTimeout 然后:开始,移动时钟(await vi.advanceTimersByTimeAsync(1)等待着它.
fireEvent
JSDOM分子的Fire Lynx事件. 在标准 Lynx 事件中使用指定的助手 :
import { fireEvent } from '@blotch/angular-lynx-testing-library';
fireEvent.tap(element);
fireEvent.input(element, { value: 'hello' });
fireEvent.scroll(element, { scrollTop: 100 });
一个被命名的帮助者运行一个处理器,在同一顺序下:
- 触摸事件发泡. 水龙头,长按或触摸 运行元素自己的
(bindtap)或者说(catchtap),然后它的先辈们在上页的路上。 所以,在一行 内可压卡 到达卡的(click)。 。 。 .(capture-bind…)财务报告和审定财务报表(capture-catch…)处理者在他们之前跑, 从一页下. - 鼠标、轮子和关键事件泡沫,正如 Lynx 在桌面和网络上发送.
- A 类
catch处理器停止事件 运行地点: a(catchtap)面板可以防止触摸到背后的背景. - 其他事件则继续发生.
scroll, (中文(简体) ),input, (中文(简体) ),layoutchange其余的只能运行 元素自己的处理器.
init.bubbles 覆盖事件本身: fireEvent.tap(element, { bubbles: false }) 只运行元素的处理器.
init.eventType 射击一种处理器 在元素上,没有其他的: fireEvent.tap(element, { eventType: 'catchEvent' }) 发送 DOM 事件 "catchEvent:tap",它只达到元素的 (catchtap)。 。 。 .
init.detail 是事件本身的数据,作为设备发送: a <image>'大小一旦装入,说.
fireEvent.load(image, { detail: { width: 240, height: 340 } });
要直接发送原始 DOM 事件 :
fireEvent(element, new Event('bindEvent:tap'));
A 类 CustomEvent是因为 detail 也到达处理器: fireEvent(element, new CustomEvent('bindEvent:load', { detail }))。 。 。 .
可用的命名助手 :
tap • 妇女 longtap • 妇女 touchstart • 妇女 touchmove • 妇女 touchcancel • 妇女 touchend • 妇女 longpress • 妇女 keydown • 妇女 keyup • 妇女 mousedown • 妇女 mouseup • 妇女 mousemove • 妇女 mouseclick • 妇女 mousedblclick • 妇女 mouselongpress • 妇女 wheel • 妇女 scroll • 妇女 scrollend • 妇女 scrolltoupper • 妇女 scrolltolower • 妇女 contentsizechanged • 妇女 scrolltoupperedge • 妇女 scrolltoloweredge • 妇女 scrolltonormalstate • 妇女 uiappear • 妇女 uidisappear • 妇女 focus • 妇女 blur • 妇女 input • 妇女 confirm • 妇女 layoutchange • 妇女 load • 妇女 error • 妇女 bgload • 妇女 bgerror • 妇女 transitionstart • 妇女 transitionend • 妇女 transitioncancel • 妇女 animationend
eventMap
Lynx事件登记簿 每个条目有一个 defaultInit 对象 : { bubbles: true } 对于事件 Lynx发送 上升的反应链, {} 剩下的钱.
import { eventMap } from '@blotch/angular-lynx-testing-library';
console.log(Object.keys(eventMap));
// ['tap', 'longtap', 'touchstart', ...]
*ByAccessibilityLabel
通过它们查找元素 accessibility-label一个屏幕阅读器读取的关于Lynx的文本 测试库 *ByLabelText 读取 <label> 财务报告和审定财务报表 aria-* 属性, Lynx 元素没有.
const { getByAccessibilityLabel } = await render(Toolbar);
fireEvent.tap(getByAccessibilityLabel('Close'));
他们来自测试图书馆的六个变体(getBy, (中文(简体) ), getAllBy, (中文(简体) ), queryBy, (中文(简体) ), queryAllBy, (中文(简体) ), findBy, (中文(简体) ), findAllBy,并取取其文本批量(字符串、 RegExp 或函数)和匹配者选项(exact, (中文(简体) ), normalizer) (中文(简体) ).
*ByRole
测试库 *ByRole,读取 Lynx 的无障碍属性就像屏幕阅读器一样. 元素测试库给出一个仍然匹配的角色,例如 <input> 作为 textbox。 。 。 .
const { getByRole } = await render(Toolbar);
fireEvent.tap(getByRole('button', { name: 'Save' }));
expect(getByRole('button', { name: 'Week', selected: true })).toBeTruthy();
accessibility-traits | 作用 |
|---|---|
button | button |
link | link |
image | img |
header | heading |
search | searchbox |
adjustable | slider |
tabbar | tablist |
- 作用: 从桌子上的每个特征。 一个
<image>是一个img除非设定accessibility-traits。 。 。 . - 可达: 屏幕阅读器到达
<text>, (中文(简体) ),<image>,其他要素accessibility-element="true"外边accessibility-elements-hiddenA级tabbar从标签周围的容器中读取,所以不需要accessibility-element。其他的则只匹配hidden: true。 。 。 . name页:1accessibility-label,或一个<text>自己发的短信 A 类<view>因为iOS不读,所以不能从里面的文字中获得名字.selected页:1selected特征,如accessibility-traits="button,selected",该活动routerLinkActive元素获得,以及[lynxAccessibilityTraits]="{ selected: true }"增加数字。 iOS读取了逗号分隔列表. Android 读取一个特性(button或者说image,并将列表作为none。 。 。 .value: { text }页:1accessibility-value。 。 。 .- 其他阿里亚州 (单位:千美元)
checked, (中文(简体) ),pressed, (中文(简体) ),expanded, (中文(简体) ),level,.)没有Lynx属性,因此Lynx元素与它们不相匹配.
screen, (中文(简体) ), within() 财务报告和审定财务报表 getQueriesForElement()
测试图书馆,有以上查询: screen 绑定每个查询 document.body,以及 within(element) 绑住他们 element。 。 。 . getQueriesForElement 实值 within。 。 。 .
queries
每个查询,不绑定(每个先取容器). 把它分散到你自己的询问中 把它们放在旁边 每个查询也单独导出, 如 getByAccessibilityLabel(container, 'Close')。 。 。 .
使用量
渲染组件
import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render } from '@blotch/angular-lynx-testing-library';
@Component({
selector: 'greeting',
imports: [LYNX_ELEMENTS],
template: `<text>Hello, {{ name }}!</text>`,
})
class GreetingComponent {
name = 'Lynx';
}
it('renders the greeting', async () => {
const { container } = await render(GreetingComponent);
expect(container.querySelector('text')!.textContent).toBe('Hello, Lynx!');
});
注射提供者
import { Component, InjectionToken, inject } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render } from '@blotch/angular-lynx-testing-library';
const GREETING = new InjectionToken<string>('GREETING');
@Component({
selector: 'test-greeting',
imports: [LYNX_ELEMENTS],
template: `<text>{{ greeting() }}</text>`,
})
class GreetingComponent {
greeting = inject(GREETING);
}
it('uses the injected value', async () => {
const { container } = await render(GreetingComponent, {
providers: [{ provide: GREETING, useValue: 'Hola' }],
});
expect(container.querySelector('text')!.textContent).toBe('Hola');
});
反应信号
定义组件外的信号,这样组件和测试都可以访问:
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
const count = signal(0);
@Component({
selector: 'test-counter',
imports: [LYNX_ELEMENTS],
template: `<text>{{ count() }}</text>`,
})
class CounterComponent {
count = count;
}
it('updates when the signal changes', async () => {
const { container } = await render(CounterComponent);
expect(container.querySelector('text')!.textContent).toBe('0');
count.set(42);
await waitForUpdate();
expect(container.querySelector('text')!.textContent).toBe('42');
});
处理事件
AngularLynx 事件绑定使用 Lynx 前缀 : (bindtap), (中文(简体) ), (catchtap), (中文(简体) ), (bindinput)等 (中文(简体) ).
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import {
render,
fireEvent,
waitForUpdate,
} from '@blotch/angular-lynx-testing-library';
@Component({
selector: 'test-button',
imports: [LYNX_ELEMENTS],
template: `
<view (bindtap)="increment()">
<text>{{ count() }}</text>
</view>
`,
})
class ButtonComponent {
count = signal(0);
increment() {
this.count.update((n) => n + 1);
}
}
it('increments on tap', async () => {
const { container } = await render(ButtonComponent);
fireEvent.tap(container.querySelector('view')!);
await waitForUpdate();
expect(container.querySelector('text')!.textContent).toBe('1');
});
控制流量—— @if
it('shows content conditionally', async () => {
const visible = signal(true);
@Component({
selector: 'test-if',
imports: [LYNX_ELEMENTS],
template: `@if (visible()) {
<text>Hello</text>
}`,
})
class IfComponent {
visible = visible;
}
const { container } = await render(IfComponent);
expect(container.querySelector('text')).not.toBeNull();
visible.set(false);
await waitForUpdate();
expect(container.querySelector('text')).toBeNull();
});
控制流量—— @for
it('renders a dynamic list', async () => {
const items = signal(['A', 'B', 'C']);
@Component({
selector: 'test-for',
imports: [LYNX_ELEMENTS],
template: `@for (item of items(); track item) {
<text>{{ item }}</text>
}`,
})
class ListComponent {
items = items;
}
const { container } = await render(ListComponent);
expect(container.querySelectorAll('text').length).toBe(3);
items.set(['A', 'B', 'C', 'D']);
await waitForUpdate();
expect(container.querySelectorAll('text').length).toBe(4);
});
结构说明
单行径模型
角链接运行 完全在 Lynx 主线上 (单位:千美元)__MAIN_THREAD__ = true) (中文(简体) ). 这与React Lynx和Vue Lynx不同,它们将渲染分出两个线程:
| 图书馆 | 线索模型 |
|---|---|
| 反应 Lynx | 双:背景线程运行 React, 主线程应用 PAPI ops |
| 韦林克 | 双:背景线程运行 Vue, 主线程应用 PAPI 操作 |
| 角线 | 单向:角成像器直接在主线上调用 PAPI |
在测试中,这意味着Angular留在主线上,即PAPI的功能所在(PAPI)__CreatePage, (中文(简体) ), __CreateView)总是有的. 环境切换到背景线程只用来回答AngularLynx发送到那里的一个呼叫,例如 LynxLocalStorage和开关直接返回.
投影管道
render(Component)
└─ bootstrapApplication(Component, {
provideZonelessChangeDetection(),
provideRenderer(),
...providers
})
└─ LynxDocument.createRootElement()
└─ __CreatePage() → <page> appended to document.body
└─ Angular renders component tree
└─ __CreateView / __CreateText / … → JSDOM children
└─ LynxRendererFactory2.end()
└─ __FlushElementTree() (no-op in tests)
└─ appRef.tick() → force synchronous CD
└─ return { container: elementTree.root, ...queries }
事件发送
通过 AngularLynx 登记事件处理器 __AddEvent(element, eventType, eventName, { type: 'worklet', value: callback })。测试环境将它作为 DOM 事件听众存储在密钥下 "${eventType}:${eventName}" (例如:. "bindEvent:tap") (中文(简体) ).
fireEvent.tap(el) 将 Lynx 的事件响应链从页面下行到 el 并备份,并呼叫每个元素的听众 对于它的处理器类型,停止一个 catch 一个 听者打电话 runWorklet(callback, [event]) 直接在主线上引用角控器。 一个出事 "bindEvent:tap" DOM活动没有用: (catchtap) 祖师相传 "catchEvent:tap" 监听者并运行操作器上方.
模板绑定 — PAPI 注册 — DOM 听器密钥 :
| 模板 | 听器密钥 |
|---|---|
(bindtap) | bindEvent:tap |
(catchtap) | catchEvent:tap |
(bindinput) | bindEvent:input |
(bindscroll) | bindEvent:scroll |
与 React/Vue Lynx 测试库的比较
| 特性 | 反应 Lynx | 韦林克 | 角线 |
|---|---|---|---|
render() 返回时 | 同步 | 同步 | 自动同步 |
| 线索模型 | 双( BG+ MT) | 双( BG+ MT) | 仅主 |
| 自动清理嵌入 var | PTL_SKIP_AUTO_CLEANUP | —— 说 | ATL_SKIP_AUTO_CLEANUP |
renderHook() | ✓ | —— 说 | —— 说 |
| 快照序列器 | (参考文献) | —— 说 | —— 说 |
| 虚拟插件 | ✓ | —— 说 | —— (中文(简体) )ng test 设立) |
| 命名为 FireEvent 助手 | 30岁以上 | 16个 | 页:1 |
| 线索意识起火 | ✓ | ✓ | - (单线) |
| Async 状态助手 | waitSchedule() | waitForUpdate() | waitForUpdate() |