import { LitElement, css, html } from 'lit' import { property } from 'lit/decorators/property.js'; import { customElement } from 'lit/decorators/custom-element.js'; import { ActorUrlTestRun, Assertion } from 'activitypub-testing/test-actor' import { repeat } from 'lit/directives/repeat.js'; /** * an element that tests an ActivityPub actor * using activitypub-testing/test-actor. * * @element activitypub-actor-tester */ @customElement('activitypub-actor-tester') export class ActivityPubActorTester extends LitElement { /** URL of the Actor that should be tested. */ @property({ type: String, reflect: true, converter(value, type) { return new URL(value ?? '', globalThis?.location?.toString()) }, }) protected actor: string; /** * whether the test should be run automatically. * If false, showing test results requires clicking a button. */ @property({ type: Boolean, reflect: true }) protected test: boolean = false; @property({ type: Array }) /** Test Assertions from the last test run */ assertions: Array> = [] @property({ attribute: false }) /** Error encountered during the last actor test run */ error: Error | undefined constructor() { super() this.actor = this.#getSearchParamsFromHash().get('actor') || globalThis.location.toString() } connectedCallback() { super.connectedCallback() if (this.test) { this.testActor() } } #getSearchParamsFromHash(url=new URL(globalThis.document.location.toString())) { const searchParams = new URLSearchParams(url.hash.replace(/^#/, '')) return searchParams } #handleFormChange(event: Event) { let form = event.target // @ts-expect-error form EventTarget if (event.target?.tagName === 'INPUT') { // @ts-expect-error form = event.target.form } // @ts-expect-error form EventTarget const formData = new FormData(form) const actor = formData.get('actor')?.valueOf() if (actor) { this.actor = String(actor) } } #handleFormSubmit(event: Event) { event.preventDefault() const form = event.target // @ts-expect-error form EventTarget const formData = new FormData(form) const actor = formData.get('actor')?.valueOf() if (actor) { this.actor = String(actor) this.testActor() } } render() { const applicableAssertions = []; const inapplicableAssertions = [] for (const assertion of this.assertions) { if (assertion.result.outcome === "inapplicable") { inapplicableAssertions.push(assertion) } else { applicableAssertions.push(assertion) } } return html`

ActivityPub Actor Tester




${this.error && html`

⚠️ Error

${this.error}

`}
${this.assertions.length ? html`

Test Results

${(0 === this.assertions.length) ? `No assertions yet. Click 'Test' above.` : ''}
    ${repeat(applicableAssertions, (assertion, index) => { const testUrl = assertion.test.url return html`
  • ${this.#renderOutcome(assertion.result.outcome)} ${assertion.test.name} ${assertion.result.outcome ? html`

    ${assertion.result.info}

    ${JSON.stringify(assertion, undefined, 2)}
    ` : "" }
  • ` })}
` : ''}
${inapplicableAssertions.length ? html`

Inapplicable Tests

These tests ran, but resulted in an inapplicable outcome.
` : ''}
` } #renderOutcome(outcome: string) { switch (outcome) { case "passed": return `✅` case "faileed": return `⛔️` default: return `⚠️` } } private async testActor(actor = new URL(this.actor)) { this.assertions = [] this.error = undefined try { for await (const assertion of new ActorUrlTestRun(actor)) { const newAssertions = [ ...this.assertions, assertion ] this.assertions = newAssertions } } catch (error) { console.warn('error iterating ActorUrlTestRun', error) if (error instanceof Error) { this.error = error } else { throw error } } } static styles = css` :host { max-width: 1280px; margin: 0 auto; } .test-url { color: inherit } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ` } declare global { interface HTMLElementTagNameMap { 'activitypub-actor-tester': ActivityPubActorTester } }