Closed18

playwrightの中身追ってみる

mehm8128mehm8128

getByRoleSelector()の全貌
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts#L59-L78

getByRoleSelector()では、internal:role=${role}の後ろに、roleとともにつけられた色々なオプションをつけて、全部合体した文字列をreturnしている

これがselectorとなって、さっき見たようにlocator()new Locator()される

playwright詳しくないのですが、locator()って普通にユーザーが使うこともあるので、internal:みたいなprefixをつけて区別してるっぽいですね、多分

https://playwright.dev/docs/api/class-locator

mehm8128mehm8128
const button = page.getByRole('button', {name: 'Click!', disabled: true}).first()

みたいにして取得したボタンを

await button.click()

してクリックしたり、

button.getByRole('img')

してボタンの中にある画像を取得したり

button.innerHTML()

してHTMLを見たりするわけなので、ここらへんの実装も見ていきます

.first()
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/locator.ts#L217-L219
selectorに追記してもう1回new Locator()するらしいです

.click()
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/locator.ts#L109-L111
frameのclick()に戻ってます

frameのclick()を見てみると、_channel.click()なるものを使ってます。後述
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/locator.ts#L109-L111

.getByRole()
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/locator.ts#L189-L191
Locatorの、.locator()を使ってます

.innerHTML()
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/locator.ts#L265-L267
frameのinnerHTML()に戻ってます

frameのinnerHTML()を見てみると、こちらも_channelなるものの.innerHTML()を使っています
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/frame.ts#L369-L371

mehm8128mehm8128

そもそもFrameChannelOwnerをextendしています
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/frame.ts#L48

ChannelOwnerはこんな感じ。一番下の_channelがさっきの_channelですね
T extends channels.Channel = channels.Channelなので、importしてるchannelsを見に行きます
https://github.com/microsoft/playwright/blob/b820ebd7e3c3e55f05a80aa6aaf35cd10c085d37/packages/playwright-core/src/client/channelOwner.ts#L28-L39

@protocol/channelsは、playwright内の別パッケージにあります
https://github.com/microsoft/playwright/tree/main/packages/protocol/src

これはおそらく、playwrightのブラウザと通信するための何かです。protocol.ymlでコード内検索かけると、いくつかスクリプトっぽいのがマッチします

なんかいつの間にか敬語になってた

mehm8128mehm8128

innerHTMLで検索かけてたら、_callOnElementOnceMatchesなる関数に辿り着きました
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/frames.ts#L1603-L1632

resolveInjectedForSelectorを追ってみると...
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/frameSelectors.ts#L171-L179

resolveFrameForSelectorに飛びます
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/frameSelectors.ts#L130-L169

_jumpToAriaRefFrameIfNeededとかいうそれらしきものを見つけたので、飛んでみます
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/frameSelectors.ts#L114-L128

'aria-ref'という何かのフラグのようなものがあるので、追ってみます
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/injectedScript.ts#L198-L231

さっき見たようなinternal:から始まるものの羅列の一番下に、aria-refがありました
しかし、先ほどの_jumpToAriaRefFrameIfNeededをよく見てみると、page.lastSnapshotFrameIdsというのがあります

また、_createAriaRefEngineに飛んでみても、同様にthis._lastAriaSnapshotを取っています
よってこれはSnapshot testing | Playwright 関連の機能で、今回は関係なさそうです(後述)
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/injectedScript.ts#L686-L692

しかし、たくさんthis._engines.setが羅列されている中で、'internal:role'もあるので一応createRoleEngine(true)の中身も見てみると...
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleSelectorEngine.ts#L182-L198

queryRoleがありました

https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleSelectorEngine.ts#L130-L180

ついにたどり着きました。どうやらこれが正解のようです
本質的には、

for (const element of root.querySelectorAll('*')) {
      match(element);

の部分でmatch(element)で全探索してマッチする要素を探しているっぽいですね
testing-libraryでは先にroleでquerySelectorAllしてたので、ここがちょっと違う部分です
https://github.com/testing-library/dom-testing-library/blob/ab9c3aeee4358cc856c14dbaec8ffc4db8f5ee76/src/queries/role.ts#L187-L190
https://github.com/testing-library/dom-testing-library/blob/ab9c3aeee4358cc856c14dbaec8ffc4db8f5ee76/src/queries/role.ts#L307-L322

_createAriaRefEngineについて補足に移ります

ここで'internal:aria-id'から'aria-ref'に変わっていて、
https://github.com/microsoft/playwright/commit/807d9066d63594cc59d324cfaf49de13c0ecd2b2#diff-06edc6b9e1c43786236ac754ec9a7ed6a4470c550a657aa6d38c2afc5dbdd850R138-R140

'internal:aria-id'はこのPRで誕生しています
https://github.com/microsoft/playwright/pull/34503

@playwright/experimental-toolsなるパッケージが作られているのですが、特にPRにもコードにも説明が書かれていないので何者なのか分かりません
packages/playwright-tools/src/examples/browser-openai.tsでAIを使った何かが行われていそうで、@playwright/experimental-toolsからはbrowserがimportされているので、AIがブラウザを操作できる何かを作っているのかもしれません

mehm8128mehm8128

全然終わってなかった
まだできてないことメモ
RoleEngineOptionsの中身読む。selectorとの繋がりが分かってない
多分、resolveFrameForSelectorからselectorを渡して、繋がる

mehm8128mehm8128

this._engines.set()InjectedScriptconstructorで実行されていたので、resolveFrameForSelectorの、const injectedScript = await context.injectedScript();に着目
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/frameSelectors.ts#L147

FrameExecutionContextinjectedScriptに辿り着き、sourcenew (module.exports.InjectedScript())を含んでいることが分かります。おそらくここでInjectedScriptを作っているのではないでしょうか
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/playwright-core/src/server/dom.ts#L84-L114

mehm8128mehm8128

終わってなかった
roleやaccessible name、aria stateなどのオプションをどのようにマッチさせてるのかを見る

roleから
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleUtils.ts#L242-L252
明示的にrole="なんとか"が付与されていれば、getExplicitAriaRole()で取得
そうでなければ、getImplicitAriaRole()でタグ名からkImplicitRoleByTagNameのマッピングで取ってきたり、presentation roleのconflictなるものの処理をしたりしている。presentation roleのconflictが何かは今度調べてみるけど、コード中にリンクも貼られている
https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleUtils.ts#L87-L188

aria stateも大体同じ。selectedで見てみる。タグ名から、何か該当する属性があればそれを確認。今回なら<option>要素のときに、selected属性を確認することができる
なければ、aria-selectedをつけてOKなrole(kAriaSelectedRolesで挙げられているrole)のときのみ、aria-selectedを確認している
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleUtils.ts#L949-L957

mehm8128mehm8128

accsessible name

nameFromがprohibitedでないroleのときのみ、getTextAlternativeInternalで計算
https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleUtils.ts#L460-L484

getTextAlternativeInternalはめっちゃ長い
step 2aとかstep 2bとか書いてあって、それぞれの説明文からも、accname1.2に沿っているようです。これはtesting-libraryで使われていたdom-accessibility-apiと同じですね
https://www.w3.org/TR/accname-1.2/#computation-steps

https://github.com/microsoft/playwright/blob/e58f076d426d8d2a70b4b6cf4010d3343116e950/packages/injected/src/roleUtils.ts#L578-L898

accname 1.2についてはこちら
https://zenn.dev/mehm8128/articles/accessible-name-and-description-computation-1-2

mehm8128mehm8128

aria-queryとかdom-accessibility-apiは最終更新がそこそこ前だから、playwrightはそこらへんも考慮して自前実装してるのかなとか思ったりした

このスクラップは3ヶ月前にクローズされました