226 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import type { Locator, Page } from '@playwright/test'
 | 
						|
import { expect } from '@playwright/test'
 | 
						|
 | 
						|
import {
 | 
						|
  checkIfPaneIsOpen,
 | 
						|
  closePane,
 | 
						|
  openPane,
 | 
						|
  sansWhitespace,
 | 
						|
} from '../test-utils'
 | 
						|
 | 
						|
interface EditorState {
 | 
						|
  activeLines: Array<string>
 | 
						|
  highlightedCode: string
 | 
						|
  diagnostics: Array<string>
 | 
						|
}
 | 
						|
 | 
						|
export class EditorFixture {
 | 
						|
  public page: Page
 | 
						|
 | 
						|
  private paneButtonTestId = 'code-pane-button'
 | 
						|
  private diagnosticsTooltip!: Locator
 | 
						|
  private diagnosticsGutterIcon!: Locator
 | 
						|
  private codeContent!: Locator
 | 
						|
  public activeLine!: Locator
 | 
						|
 | 
						|
  constructor(page: Page) {
 | 
						|
    this.page = page
 | 
						|
    this.codeContent = page.locator('.cm-content[data-language="kcl"]')
 | 
						|
    this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
 | 
						|
    this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
 | 
						|
    this.activeLine = this.page.locator('.cm-activeLine')
 | 
						|
  }
 | 
						|
 | 
						|
  private _expectEditorToContain =
 | 
						|
    (not = false) =>
 | 
						|
    async (
 | 
						|
      code: string,
 | 
						|
      {
 | 
						|
        shouldNormalise = false,
 | 
						|
        timeout = 5_000,
 | 
						|
      }: { shouldNormalise?: boolean; timeout?: number } = {}
 | 
						|
    ) => {
 | 
						|
      const wasPaneOpen = await this.checkIfPaneIsOpen()
 | 
						|
      if (!wasPaneOpen) {
 | 
						|
        await this.openPane()
 | 
						|
      }
 | 
						|
      const resetPane = async () => {
 | 
						|
        if (!wasPaneOpen) {
 | 
						|
          await this.closePane()
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (!shouldNormalise) {
 | 
						|
        const expectStart = expect.poll(() => this.codeContent.textContent())
 | 
						|
        if (not) {
 | 
						|
          const result = await expectStart.not.toContain(code)
 | 
						|
          await resetPane()
 | 
						|
          return result
 | 
						|
        }
 | 
						|
        const result = await expectStart.toContain(code)
 | 
						|
        await resetPane()
 | 
						|
        return result
 | 
						|
      }
 | 
						|
      const normalisedCode = code.replaceAll(/\s+/g, '').trim()
 | 
						|
      const expectStart = expect.poll(
 | 
						|
        async () => {
 | 
						|
          const editorText = await this.codeContent.textContent()
 | 
						|
          return editorText?.replaceAll(/\s+/g, '').trim()
 | 
						|
        },
 | 
						|
        {
 | 
						|
          timeout,
 | 
						|
        }
 | 
						|
      )
 | 
						|
      if (not) {
 | 
						|
        const result = await expectStart.not.toContain(normalisedCode)
 | 
						|
        await resetPane()
 | 
						|
        return result
 | 
						|
      }
 | 
						|
      const result = await expectStart.toContain(normalisedCode)
 | 
						|
      await resetPane()
 | 
						|
      return result
 | 
						|
    }
 | 
						|
  expectEditor = {
 | 
						|
    toContain: this._expectEditorToContain(),
 | 
						|
    not: { toContain: this._expectEditorToContain(true) },
 | 
						|
    toBe: async (code: string) => {
 | 
						|
      const currentCode = await this.getCurrentCode()
 | 
						|
      return expect(currentCode).toBe(code)
 | 
						|
    },
 | 
						|
  }
 | 
						|
  getCurrentCode = async () => {
 | 
						|
    return await this.codeContent.innerText()
 | 
						|
  }
 | 
						|
  snapshot = async (options?: { timeout?: number; name?: string }) => {
 | 
						|
    const wasPaneOpen = await this.checkIfPaneIsOpen()
 | 
						|
    if (!wasPaneOpen) {
 | 
						|
      await this.openPane()
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      // Use expect.poll to implement retry logic
 | 
						|
      await expect
 | 
						|
        .poll(
 | 
						|
          async () => {
 | 
						|
            const code = await this.codeContent.textContent()
 | 
						|
            return code || ''
 | 
						|
          },
 | 
						|
          { timeout: options?.timeout || 5000 }
 | 
						|
        )
 | 
						|
        .toMatchSnapshot(options?.name || 'editor-content')
 | 
						|
    } finally {
 | 
						|
      // Reset pane state if needed
 | 
						|
      if (!wasPaneOpen) {
 | 
						|
        await this.closePane()
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  private _serialiseDiagnostics = async (): Promise<Array<string>> => {
 | 
						|
    const diagnostics = await this.diagnosticsGutterIcon.all()
 | 
						|
    const diagnosticsContent: string[] = []
 | 
						|
    for (const diag of diagnostics) {
 | 
						|
      await diag.hover()
 | 
						|
      const content = await this.diagnosticsTooltip.allTextContents()
 | 
						|
      diagnosticsContent.push(content.join(''))
 | 
						|
    }
 | 
						|
    return [...new Set(diagnosticsContent)].map((d) => sansWhitespace(d))
 | 
						|
  }
 | 
						|
 | 
						|
  private _getHighlightedCode = async () => {
 | 
						|
    const texts = (
 | 
						|
      await this.page.getByTestId('hover-highlight').allInnerTexts()
 | 
						|
    ).map((s) => s.replace(/\s+/g, '').trim())
 | 
						|
    return texts.join('')
 | 
						|
  }
 | 
						|
  private _getActiveLines = async () =>
 | 
						|
    (await this.activeLine.allInnerTexts()).map((l) => l.trim())
 | 
						|
  expectActiveLinesToBe = async (lines: Array<string>) => {
 | 
						|
    await expect.poll(this._getActiveLines).toEqual(lines.map((l) => l.trim()))
 | 
						|
  }
 | 
						|
  /** assert all editor state EXCEPT the code content */
 | 
						|
  expectState = async (expectedState: EditorState) => {
 | 
						|
    await expect
 | 
						|
      .poll(async () => {
 | 
						|
        const [activeLines, highlightedCode, diagnostics] = await Promise.all([
 | 
						|
          this._getActiveLines(),
 | 
						|
          this._getHighlightedCode(),
 | 
						|
          this._serialiseDiagnostics(),
 | 
						|
        ])
 | 
						|
        const state: EditorState = {
 | 
						|
          activeLines: activeLines.map(sansWhitespace).filter(Boolean),
 | 
						|
          highlightedCode: sansWhitespace(highlightedCode),
 | 
						|
          diagnostics,
 | 
						|
        }
 | 
						|
        return state
 | 
						|
      })
 | 
						|
      .toEqual({
 | 
						|
        activeLines: expectedState.activeLines.map(sansWhitespace),
 | 
						|
        highlightedCode: sansWhitespace(expectedState.highlightedCode),
 | 
						|
        diagnostics: expectedState.diagnostics.map(sansWhitespace),
 | 
						|
      })
 | 
						|
  }
 | 
						|
  replaceCode = async (findCode: string, replaceCode: string) => {
 | 
						|
    const lines = await this.page.locator('.cm-line').all()
 | 
						|
 | 
						|
    let code = (await Promise.all(lines.map((c) => c.textContent()))).join('\n')
 | 
						|
    if (!findCode) {
 | 
						|
      // nuke everything
 | 
						|
      code = replaceCode
 | 
						|
    } else {
 | 
						|
      if (!lines) return
 | 
						|
      code = code.replace(findCode, replaceCode)
 | 
						|
    }
 | 
						|
    await this.codeContent.fill(code)
 | 
						|
  }
 | 
						|
  checkIfPaneIsOpen() {
 | 
						|
    return checkIfPaneIsOpen(this.page, this.paneButtonTestId)
 | 
						|
  }
 | 
						|
  closePane() {
 | 
						|
    return closePane(this.page, this.paneButtonTestId)
 | 
						|
  }
 | 
						|
  openPane() {
 | 
						|
    return openPane(this.page, this.paneButtonTestId)
 | 
						|
  }
 | 
						|
  scrollToText(text: string, placeCursor?: boolean) {
 | 
						|
    return this.page.evaluate(
 | 
						|
      (args: { text: string; placeCursor?: boolean }) => {
 | 
						|
        // error TS2339: Property 'docView' does not exist on type 'EditorView'.
 | 
						|
        // Except it does so :shrug:
 | 
						|
        // @ts-ignore
 | 
						|
        let index = window.editorManager._editorView?.docView.view.state.doc
 | 
						|
          .toString()
 | 
						|
          .indexOf(args.text)
 | 
						|
        window.editorManager._editorView?.focus()
 | 
						|
        window.editorManager._editorView?.dispatch({
 | 
						|
          selection: window.EditorSelection.create([
 | 
						|
            window.EditorSelection.cursor(index),
 | 
						|
          ]),
 | 
						|
          effects: [
 | 
						|
            window.EditorView.scrollIntoView(
 | 
						|
              window.EditorSelection.range(index, index + 1)
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        })
 | 
						|
      },
 | 
						|
      { text, placeCursor }
 | 
						|
    )
 | 
						|
  }
 | 
						|
  async selectText(text: string) {
 | 
						|
    // First make sure the code pane is open
 | 
						|
    const wasPaneOpen = await this.checkIfPaneIsOpen()
 | 
						|
    if (!wasPaneOpen) {
 | 
						|
      await this.openPane()
 | 
						|
    }
 | 
						|
 | 
						|
    // Use Playwright's built-in text selection on the code content
 | 
						|
    // it seems to only select whole divs, which works out to align with syntax highlighting
 | 
						|
    // for code mirror, so you can probably select "sketch002 = startSketchOn(XZ)"
 | 
						|
    // but less so for exactly "sketch002 = startS"
 | 
						|
    await this.codeContent.getByText(text).first().selectText()
 | 
						|
 | 
						|
    // Reset pane state if needed
 | 
						|
    if (!wasPaneOpen) {
 | 
						|
      await this.closePane()
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |