From bef1061c064e5135d86a48fffd7278f3e7f4a29e Mon Sep 17 00:00:00 2001 From: Joe Gomain Hoyes Date: Wed, 23 Nov 2022 16:59:23 +0700 Subject: [PATCH] feat(puppeteer-core): Infer element type from complex selector (#9253) **What kind of change does this PR introduce?** Better type inference. **Did you add tests for your changes?** ~Not yet.~ Yes. **If relevant, did you update the documentation?** Not yet. **Summary** Currently methods that return an element handle, i.e. `.$`, `.waitForSelector` attempt to infer the node element type from the selector string. However, this only works when the selector is an exact match of the element tag, i.e. a selector `"a"` would be inferred as `HTMLAnchorElement` . And not when the selector is complex, i.e. selectors `"a#some-id"`, `div > a`, `a:nth-child(2)` would all fallback on `Element`. This is due to simply looking up the the selector in `HTMLElementTagNameMap` and `SVGElementTagNameMap` without any attempt to parse the selector string. This PR is an attempt to do so. **Does this PR introduce a breaking change?** This could break existing incorrect assertions using the `as` keyword. **Other information** ~This PR introduces a dependency on the `type-fest` package.~ This PR is far from complete (no tests, no docs). Put out early for feedback and discussion. Co-authored-by: Alex Rudenko --- docs/api/puppeteer.nodefor.md | 14 +- packages/puppeteer-core/src/common/types.ts | 103 ++- test-d/ElementHandle.test-d.ts | 886 +++++++++++++++++++- test-d/NodeFor.test-d.ts | 144 ++++ 4 files changed, 1125 insertions(+), 22 deletions(-) create mode 100644 test-d/NodeFor.test-d.ts diff --git a/docs/api/puppeteer.nodefor.md b/docs/api/puppeteer.nodefor.md index f575d3b0..3099cf1a 100644 --- a/docs/api/puppeteer.nodefor.md +++ b/docs/api/puppeteer.nodefor.md @@ -7,10 +7,12 @@ sidebar_label: NodeFor #### Signature: ```typescript -export declare type NodeFor = - Selector extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[Selector] - : Selector extends keyof SVGElementTagNameMap - ? SVGElementTagNameMap[Selector] - : Element; +export declare type NodeFor = + TypeSelectorOfComplexSelector extends infer TypeSelector + ? TypeSelector extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[TypeSelector] + : TypeSelector extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[TypeSelector] + : Element + : never; ``` diff --git a/packages/puppeteer-core/src/common/types.ts b/packages/puppeteer-core/src/common/types.ts index e2938862..ccbc231c 100644 --- a/packages/puppeteer-core/src/common/types.ts +++ b/packages/puppeteer-core/src/common/types.ts @@ -67,9 +67,100 @@ export type EvaluateFunc = ( /** * @public */ -export type NodeFor = - Selector extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[Selector] - : Selector extends keyof SVGElementTagNameMap - ? SVGElementTagNameMap[Selector] - : Element; +export type NodeFor = + TypeSelectorOfComplexSelector extends infer TypeSelector + ? TypeSelector extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[TypeSelector] + : TypeSelector extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[TypeSelector] + : Element + : never; + +type TypeSelectorOfComplexSelector = + CompoundSelectorsOfComplexSelector extends infer CompoundSelectors + ? CompoundSelectors extends NonEmptyReadonlyArray + ? Last extends infer LastCompoundSelector + ? LastCompoundSelector extends string + ? TypeSelectorOfCompoundSelector + : never + : never + : unknown + : never; + +type TypeSelectorOfCompoundSelector = + SplitWithDelemiters< + CompoundSelector, + BeginSubclassSelectorTokens + > extends infer CompoundSelectorTokens + ? CompoundSelectorTokens extends [infer TypeSelector, ...any[]] + ? TypeSelector extends '' + ? unknown + : TypeSelector + : never + : never; + +type Last> = Arr extends [ + infer Head, + ...infer Tail +] + ? Tail extends NonEmptyReadonlyArray + ? Last + : Head + : never; + +type NonEmptyReadonlyArray = [T, ...(readonly T[])]; + +type CompoundSelectorsOfComplexSelector = + SplitWithDelemiters< + ComplexSelector, + CombinatorTokens + > extends infer IntermediateTokens + ? IntermediateTokens extends readonly string[] + ? Drop + : never + : never; + +type SplitWithDelemiters< + Input extends string, + Delemiters extends readonly string[] +> = Delemiters extends [infer FirstDelemiter, ...infer RestDelemiters] + ? FirstDelemiter extends string + ? RestDelemiters extends readonly string[] + ? FlatmapSplitWithDelemiters, RestDelemiters> + : never + : never + : [Input]; + +type BeginSubclassSelectorTokens = ['.', '#', '[', ':']; + +type CombinatorTokens = [' ', '>', '+', '~', '|', '|']; + +type Drop = Arr extends [ + infer Head, + ...infer Tail +] + ? Head extends Remove + ? Drop + : [Head, ...Drop] + : []; + +type FlatmapSplitWithDelemiters< + Inputs extends readonly string[], + Delemiters extends readonly string[] +> = Inputs extends [infer FirstInput, ...infer RestInputs] + ? FirstInput extends string + ? RestInputs extends readonly string[] + ? [ + ...SplitWithDelemiters, + ...FlatmapSplitWithDelemiters + ] + : never + : never + : []; + +type Split< + Input extends string, + Delemiter extends string +> = Input extends `${infer Prefix}${Delemiter}${infer Suffix}` + ? [Prefix, ...Split] + : [Input]; diff --git a/test-d/ElementHandle.test-d.ts b/test-d/ElementHandle.test-d.ts index 55b511ab..e23e6667 100644 --- a/test-d/ElementHandle.test-d.ts +++ b/test-d/ElementHandle.test-d.ts @@ -5,29 +5,895 @@ declare const handle: ElementHandle; { { - expectType | null>(await handle.$('a')); - expectNotType | null>(await handle.$('a')); + { + expectType | null>(await handle.$('a')); + expectNotType | null>(await handle.$('a')); + } + { + expectType | null>( + await handle.$('a#id') + ); + expectNotType | null>(await handle.$('a#id')); + } + { + expectType | null>( + await handle.$('a.class') + ); + expectNotType | null>(await handle.$('a.class')); + } + { + expectType | null>( + await handle.$('a[attr=value]') + ); + expectNotType | null>( + await handle.$('a[attr=value]') + ); + } + { + expectType | null>( + await handle.$('a:psuedo-class') + ); + expectNotType | null>( + await handle.$('a:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('a:func(arg)') + ); + expectNotType | null>( + await handle.$('a:func(arg)') + ); + } } { - expectType | null>(await handle.$('div')); - expectNotType | null>(await handle.$('div')); + { + expectType | null>(await handle.$('div')); + expectNotType | null>(await handle.$('div')); + } + { + expectType | null>( + await handle.$('div#id') + ); + expectNotType | null>(await handle.$('div#id')); + } + { + expectType | null>( + await handle.$('div.class') + ); + expectNotType | null>(await handle.$('div.class')); + } + { + expectType | null>( + await handle.$('div[attr=value]') + ); + expectNotType | null>( + await handle.$('div[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div:psuedo-class') + ); + expectNotType | null>( + await handle.$('div:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div:func(arg)') + ); + expectNotType | null>( + await handle.$('div:func(arg)') + ); + } } { - expectType | null>(await handle.$('some-custom')); + { + expectType | null>(await handle.$('some-custom')); + } + { + expectType | null>( + await handle.$('some-custom#id') + ); + } + { + expectType | null>( + await handle.$('some-custom.class') + ); + } + { + expectType | null>( + await handle.$('some-custom[attr=value]') + ); + } + { + expectType | null>( + await handle.$('some-custom:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('some-custom:func(arg)') + ); + } + } + { + { + expectType | null>(await handle.$('')); + } + { + expectType | null>(await handle.$('#id')); + } + { + expectType | null>(await handle.$('.class')); + } + { + expectType | null>(await handle.$('[attr=value]')); + } + { + expectType | null>( + await handle.$(':pseudo-class') + ); + } + { + expectType | null>(await handle.$(':func(arg)')); + } + } + { + { + expectType | null>( + await handle.$('div > a') + ); + expectNotType | null>(await handle.$('div > a')); + } + { + expectType | null>( + await handle.$('div > a#id') + ); + expectNotType | null>( + await handle.$('div > a#id') + ); + } + { + expectType | null>( + await handle.$('div > a.class') + ); + expectNotType | null>( + await handle.$('div > a.class') + ); + } + { + expectType | null>( + await handle.$('div > a[attr=value]') + ); + expectNotType | null>( + await handle.$('div > a[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > a:psuedo-class') + ); + expectNotType | null>( + await handle.$('div > a:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > a:func(arg)') + ); + expectNotType | null>( + await handle.$('div > a:func(arg)') + ); + } + } + { + { + expectType | null>( + await handle.$('div > div') + ); + expectNotType | null>(await handle.$('div > div')); + } + { + expectType | null>( + await handle.$('div > div#id') + ); + expectNotType | null>( + await handle.$('div > div#id') + ); + } + { + expectType | null>( + await handle.$('div > div.class') + ); + expectNotType | null>( + await handle.$('div > div.class') + ); + } + { + expectType | null>( + await handle.$('div > div[attr=value]') + ); + expectNotType | null>( + await handle.$('div > div[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > div:psuedo-class') + ); + expectNotType | null>( + await handle.$('div > div:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > div:func(arg)') + ); + expectNotType | null>( + await handle.$('div > div:func(arg)') + ); + } + } + { + { + expectType | null>( + await handle.$('div > some-custom') + ); + } + { + expectType | null>( + await handle.$('div > some-custom#id') + ); + } + { + expectType | null>( + await handle.$('div > some-custom.class') + ); + } + { + expectType | null>( + await handle.$('div > some-custom[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > some-custom:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType | null>(await handle.$('div > #id')); + } + { + expectType | null>(await handle.$('div > .class')); + } + { + expectType | null>( + await handle.$('div > [attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > :pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > :func(arg)') + ); + } + } + { + { + expectType | null>( + await handle.$('div > a') + ); + expectNotType | null>(await handle.$('div > a')); + } + { + expectType | null>( + await handle.$('div > a#id') + ); + expectNotType | null>( + await handle.$('div > a#id') + ); + } + { + expectType | null>( + await handle.$('div > a.class') + ); + expectNotType | null>( + await handle.$('div > a.class') + ); + } + { + expectType | null>( + await handle.$('div > a[attr=value]') + ); + expectNotType | null>( + await handle.$('div > a[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > a:psuedo-class') + ); + expectNotType | null>( + await handle.$('div > a:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > a:func(arg)') + ); + expectNotType | null>( + await handle.$('div > a:func(arg)') + ); + } + } + { + { + expectType | null>( + await handle.$('div > div') + ); + expectNotType | null>(await handle.$('div > div')); + } + { + expectType | null>( + await handle.$('div > div#id') + ); + expectNotType | null>( + await handle.$('div > div#id') + ); + } + { + expectType | null>( + await handle.$('div > div.class') + ); + expectNotType | null>( + await handle.$('div > div.class') + ); + } + { + expectType | null>( + await handle.$('div > div[attr=value]') + ); + expectNotType | null>( + await handle.$('div > div[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > div:psuedo-class') + ); + expectNotType | null>( + await handle.$('div > div:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > div:func(arg)') + ); + expectNotType | null>( + await handle.$('div > div:func(arg)') + ); + } + } + { + { + expectType | null>( + await handle.$('div > some-custom') + ); + } + { + expectType | null>( + await handle.$('div > some-custom#id') + ); + } + { + expectType | null>( + await handle.$('div > some-custom.class') + ); + } + { + expectType | null>( + await handle.$('div > some-custom[attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > some-custom:pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType | null>(await handle.$('div > #id')); + } + { + expectType | null>(await handle.$('div > .class')); + } + { + expectType | null>( + await handle.$('div > [attr=value]') + ); + } + { + expectType | null>( + await handle.$('div > :pseudo-class') + ); + } + { + expectType | null>( + await handle.$('div > :func(arg)') + ); + } } } { { - expectType>>(await handle.$$('a')); - expectNotType>>(await handle.$$('a')); + { + expectType>>(await handle.$$('a')); + expectNotType>>(await handle.$$('a')); + } + { + expectType>>( + await handle.$$('a#id') + ); + expectNotType>>(await handle.$$('a#id')); + } + { + expectType>>( + await handle.$$('a.class') + ); + expectNotType>>(await handle.$$('a.class')); + } + { + expectType>>( + await handle.$$('a[attr=value]') + ); + expectNotType>>( + await handle.$$('a[attr=value]') + ); + } + { + expectType>>( + await handle.$$('a:psuedo-class') + ); + expectNotType>>( + await handle.$$('a:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('a:func(arg)') + ); + expectNotType>>( + await handle.$$('a:func(arg)') + ); + } } { - expectType>>(await handle.$$('div')); - expectNotType>>(await handle.$$('div')); + { + expectType>>(await handle.$$('div')); + expectNotType>>(await handle.$$('div')); + } + { + expectType>>( + await handle.$$('div#id') + ); + expectNotType>>(await handle.$$('div#id')); + } + { + expectType>>( + await handle.$$('div.class') + ); + expectNotType>>( + await handle.$$('div.class') + ); + } + { + expectType>>( + await handle.$$('div[attr=value]') + ); + expectNotType>>( + await handle.$$('div[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div:psuedo-class') + ); + expectNotType>>( + await handle.$$('div:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div:func(arg)') + ); + expectNotType>>( + await handle.$$('div:func(arg)') + ); + } } { - expectType>>(await handle.$$('some-custom')); + { + expectType>>(await handle.$$('some-custom')); + } + { + expectType>>( + await handle.$$('some-custom#id') + ); + } + { + expectType>>( + await handle.$$('some-custom.class') + ); + } + { + expectType>>( + await handle.$$('some-custom[attr=value]') + ); + } + { + expectType>>( + await handle.$$('some-custom:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('some-custom:func(arg)') + ); + } + } + { + { + expectType>>(await handle.$$('')); + } + { + expectType>>(await handle.$$('#id')); + } + { + expectType>>(await handle.$$('.class')); + } + { + expectType>>( + await handle.$$('[attr=value]') + ); + } + { + expectType>>( + await handle.$$(':pseudo-class') + ); + } + { + expectType>>(await handle.$$(':func(arg)')); + } + } + { + { + expectType>>( + await handle.$$('div > a') + ); + expectNotType>>(await handle.$$('div > a')); + } + { + expectType>>( + await handle.$$('div > a#id') + ); + expectNotType>>( + await handle.$$('div > a#id') + ); + } + { + expectType>>( + await handle.$$('div > a.class') + ); + expectNotType>>( + await handle.$$('div > a.class') + ); + } + { + expectType>>( + await handle.$$('div > a[attr=value]') + ); + expectNotType>>( + await handle.$$('div > a[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > a:psuedo-class') + ); + expectNotType>>( + await handle.$$('div > a:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > a:func(arg)') + ); + expectNotType>>( + await handle.$$('div > a:func(arg)') + ); + } + } + { + { + expectType>>( + await handle.$$('div > div') + ); + expectNotType>>( + await handle.$$('div > div') + ); + } + { + expectType>>( + await handle.$$('div > div#id') + ); + expectNotType>>( + await handle.$$('div > div#id') + ); + } + { + expectType>>( + await handle.$$('div > div.class') + ); + expectNotType>>( + await handle.$$('div > div.class') + ); + } + { + expectType>>( + await handle.$$('div > div[attr=value]') + ); + expectNotType>>( + await handle.$$('div > div[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > div:psuedo-class') + ); + expectNotType>>( + await handle.$$('div > div:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > div:func(arg)') + ); + expectNotType>>( + await handle.$$('div > div:func(arg)') + ); + } + } + { + { + expectType>>( + await handle.$$('div > some-custom') + ); + } + { + expectType>>( + await handle.$$('div > some-custom#id') + ); + } + { + expectType>>( + await handle.$$('div > some-custom.class') + ); + } + { + expectType>>( + await handle.$$('div > some-custom[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > some-custom:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType>>(await handle.$$('div > #id')); + } + { + expectType>>( + await handle.$$('div > .class') + ); + } + { + expectType>>( + await handle.$$('div > [attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > :pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > :func(arg)') + ); + } + } + { + { + expectType>>( + await handle.$$('div > a') + ); + expectNotType>>(await handle.$$('div > a')); + } + { + expectType>>( + await handle.$$('div > a#id') + ); + expectNotType>>( + await handle.$$('div > a#id') + ); + } + { + expectType>>( + await handle.$$('div > a.class') + ); + expectNotType>>( + await handle.$$('div > a.class') + ); + } + { + expectType>>( + await handle.$$('div > a[attr=value]') + ); + expectNotType>>( + await handle.$$('div > a[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > a:psuedo-class') + ); + expectNotType>>( + await handle.$$('div > a:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > a:func(arg)') + ); + expectNotType>>( + await handle.$$('div > a:func(arg)') + ); + } + } + { + { + expectType>>( + await handle.$$('div > div') + ); + expectNotType>>( + await handle.$$('div > div') + ); + } + { + expectType>>( + await handle.$$('div > div#id') + ); + expectNotType>>( + await handle.$$('div > div#id') + ); + } + { + expectType>>( + await handle.$$('div > div.class') + ); + expectNotType>>( + await handle.$$('div > div.class') + ); + } + { + expectType>>( + await handle.$$('div > div[attr=value]') + ); + expectNotType>>( + await handle.$$('div > div[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > div:psuedo-class') + ); + expectNotType>>( + await handle.$$('div > div:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > div:func(arg)') + ); + expectNotType>>( + await handle.$$('div > div:func(arg)') + ); + } + } + { + { + expectType>>( + await handle.$$('div > some-custom') + ); + } + { + expectType>>( + await handle.$$('div > some-custom#id') + ); + } + { + expectType>>( + await handle.$$('div > some-custom.class') + ); + } + { + expectType>>( + await handle.$$('div > some-custom[attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > some-custom:pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType>>(await handle.$$('div > #id')); + } + { + expectType>>( + await handle.$$('div > .class') + ); + } + { + expectType>>( + await handle.$$('div > [attr=value]') + ); + } + { + expectType>>( + await handle.$$('div > :pseudo-class') + ); + } + { + expectType>>( + await handle.$$('div > :func(arg)') + ); + } } } diff --git a/test-d/NodeFor.test-d.ts b/test-d/NodeFor.test-d.ts new file mode 100644 index 00000000..096dccaf --- /dev/null +++ b/test-d/NodeFor.test-d.ts @@ -0,0 +1,144 @@ +import type {NodeFor} from 'puppeteer'; +import {expectType, expectNotType} from 'tsd'; + +declare const nodeFor: ( + selector: Selector +) => NodeFor; + +{ + { + expectType(nodeFor('a')); + expectNotType(nodeFor('a')); + } + { + expectType(nodeFor('a#ignored')); + expectNotType(nodeFor('a#ignored')); + } + { + expectType(nodeFor('a.ignored')); + expectNotType(nodeFor('a.ignored')); + } + { + expectType(nodeFor('a[ignored')); + expectNotType(nodeFor('a[ignored')); + } + { + expectType(nodeFor('a:ignored')); + expectNotType(nodeFor('a:ignored')); + } + { + expectType(nodeFor('ignored a')); + expectNotType(nodeFor('ignored a')); + } + { + expectType(nodeFor('ignored a#ignored')); + expectNotType(nodeFor('ignored a#ignored')); + } + { + expectType(nodeFor('ignored a.ignored')); + expectNotType(nodeFor('ignored a.ignored')); + } + { + expectType(nodeFor('ignored a[ignored')); + expectNotType(nodeFor('ignored a[ignored')); + } + { + expectType(nodeFor('ignored a:ignored')); + expectNotType(nodeFor('ignored a:ignored')); + } + { + expectType(nodeFor('ignored > a')); + expectNotType(nodeFor('ignored > a')); + } + { + expectType(nodeFor('ignored > a#ignored')); + expectNotType(nodeFor('ignored > a#ignored')); + } + { + expectType(nodeFor('ignored > a.ignored')); + expectNotType(nodeFor('ignored > a.ignored')); + } + { + expectType(nodeFor('ignored > a[ignored')); + expectNotType(nodeFor('ignored > a[ignored')); + } + { + expectType(nodeFor('ignored > a:ignored')); + expectNotType(nodeFor('ignored > a:ignored')); + } + { + expectType(nodeFor('ignored + a')); + expectNotType(nodeFor('ignored + a')); + } + { + expectType(nodeFor('ignored ~ a')); + expectNotType(nodeFor('ignored ~ a')); + } + { + expectType(nodeFor('ignored | a')); + expectNotType(nodeFor('ignored | a')); + } + { + expectType( + nodeFor('ignored ignored > ignored + ignored | a#ignore') + ); + expectNotType( + nodeFor('ignored ignored > ignored + ignored | a#ignore') + ); + } +} +{ + { + expectType(nodeFor('')); + } + { + expectType(nodeFor('#ignored')); + } + { + expectType(nodeFor('.ignored')); + } + { + expectType(nodeFor('[ignored')); + } + { + expectType(nodeFor(':ignored')); + } + { + expectType(nodeFor('ignored #ignored')); + } + { + expectType(nodeFor('ignored .ignored')); + } + { + expectType(nodeFor('ignored [ignored')); + } + { + expectType(nodeFor('ignored :ignored')); + } + { + expectType(nodeFor('ignored > #ignored')); + } + { + expectType(nodeFor('ignored > .ignored')); + } + { + expectType(nodeFor('ignored > [ignored')); + } + { + expectType(nodeFor('ignored > :ignored')); + } + { + expectType(nodeFor('ignored + #ignored')); + } + { + expectType(nodeFor('ignored ~ #ignored')); + } + { + expectType(nodeFor('ignored | #ignored')); + } + { + expectType( + nodeFor('ignored ignored > ignored ~ ignored + ignored | #ignored') + ); + } +}