@@ -449,10 +479,3 @@ declare global {
[EXPLORER]: DevtoolsSidebarExplorer
}
}
-
-function getSearchableLabel(entry: TestEntry): string[] {
- if (entry.children.length === 0) {
- return [entry.label]
- }
- return entry.children.flatMap(getSearchableLabel)
-}
diff --git a/packages/app/src/components/sidebar/filter.ts b/packages/app/src/components/sidebar/filter.ts
index 7ee639ff..18bdb85b 100644
--- a/packages/app/src/components/sidebar/filter.ts
+++ b/packages/app/src/components/sidebar/filter.ts
@@ -2,33 +2,51 @@ import { Element } from '@core/element'
import { html, css } from 'lit'
import { customElement, query } from 'lit/decorators.js'
-import '~icons/mdi/chevron-right.js'
-
-enum FilterState {
- PASSED = 1,
- FAILED = 2,
- SKIPPED = 4
-}
+import '~icons/mdi/magnify.js'
@customElement('wdio-devtools-sidebar-filter')
export class DevtoolsSidebarFilter extends Element {
- #filterState = 0
#filterQuery = ''
- #isStateFilterOpen = false
static styles = [
...Element.styles,
css`
:host {
+ display: block;
width: 100%;
- display: flex;
- align-items: top;
- font-size: 0.8em;
- padding-right: 1em !important;
+ font-size: 12px;
}
- label {
- cursor: pointer;
+ .field {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ height: 2rem;
+ padding: 0 0.6rem;
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 8px;
+ background: var(--vscode-input-background);
+ box-shadow: 0 1px 2px var(--vscode-widget-shadow);
+ transition: border-color 0.12s;
+ }
+ .field:focus-within {
+ border-color: var(--accent);
+ }
+ .field icon-mdi-magnify {
+ flex: none;
+ width: 1rem;
+ height: 1rem;
+ display: block;
+ color: var(--vscode-descriptionForeground);
+ }
+ .field input {
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ border: none;
+ background: none;
+ color: var(--vscode-input-foreground);
+ outline: none;
}
`
]
@@ -36,18 +54,6 @@ export class DevtoolsSidebarFilter extends Element {
@query('input[name="filter"]')
queryInput?: HTMLInputElement
- #updateState(change: Event) {
- const target = change.target as HTMLInputElement | null
- if (!target) {
- return
- }
- this.#filterState = target.checked
- ? this.#filterState + Number(target.value)
- : this.#filterState - Number(target.value)
- this.requestUpdate()
- this.#emitState()
- }
-
#updateQuery() {
if (!this.queryInput) {
return
@@ -56,11 +62,6 @@ export class DevtoolsSidebarFilter extends Element {
this.#emitState()
}
- #toggleStateFilter() {
- this.#isStateFilterOpen = !this.#isStateFilterOpen
- this.requestUpdate()
- }
-
#emitState() {
window.dispatchEvent(
new CustomEvent('app-test-filter', {
@@ -71,82 +72,20 @@ export class DevtoolsSidebarFilter extends Element {
)
}
- get filtersPassed() {
- return this.#filterState & FilterState.PASSED
- }
- get filtersFailed() {
- return this.#filterState & FilterState.FAILED
- }
- get filtersSkipped() {
- return this.#filterState & FilterState.SKIPPED
- }
- get filterStatus() {
- if (this.filtersPassed && this.filtersFailed && this.filtersSkipped) {
- return 'all'
- }
-
- return (
- ['passed', 'failed', 'skipped']
- .filter(
- (filter) =>
- this[
- `filters${filter.charAt(0).toUpperCase() + filter.slice(1)}` as keyof typeof this
- ]
- )
- .join(', ') || 'none'
- )
- }
get filterQuery() {
return this.#filterQuery
}
- #renderStateCheckbox(state: FilterState, label: string, id: string) {
- return html`
-
+
- ${visiblePairs.map((pair) =>
- this.#renderPair(pair, leftCommands, rightCommands, firstDivergent)
- )}
+
+
+
+ ${visiblePairs.map((pair) =>
+ this.#renderPair(pair, leftCommands, rightCommands, firstDivergent)
+ )}
+
`
}
diff --git a/packages/app/src/components/workbench/compare/markers.ts b/packages/app/src/components/workbench/compare/markers.ts
index ed87c5a9..ccec430b 100644
--- a/packages/app/src/components/workbench/compare/markers.ts
+++ b/packages/app/src/components/workbench/compare/markers.ts
@@ -98,11 +98,13 @@ export function renderMarker(
}
const statusMarker = renderStatusMarker(cmd, step, allCmdsThisSide)
if (kind === 'missing' && !oneSideEntirelyEmpty) {
- return html`${statusMarker}
only here `
+ >${statusMarker}`
}
return statusMarker
}
diff --git a/packages/app/src/components/workbench/compare/styles.ts b/packages/app/src/components/workbench/compare/styles.ts
index 3b8cadc8..9fe9ceb1 100644
--- a/packages/app/src/components/workbench/compare/styles.ts
+++ b/packages/app/src/components/workbench/compare/styles.ts
@@ -14,192 +14,279 @@ export const compareStyles = css`
background-color: var(--vscode-editor-background, #1e1e1e);
color: var(--vscode-foreground, #cccccc);
}
- .compare-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0;
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- /* Stack rows from the top so they don't stretch to fill the grid. */
- align-content: start;
- grid-auto-rows: min-content;
- }
- .step-row {
- display: contents;
+
+ /* ββ Toolbar ββ */
+ .topbar {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--vscode-panel-border);
}
- .step-cell {
- padding: 0.25rem 0.5rem;
- border-bottom: 1px solid var(--vscode-panel-border, #2a2a2a);
+ .pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
font-family: var(--vscode-editor-font-family, monospace);
- font-size: 0.85em;
- cursor: pointer;
+ font-size: 11.5px;
+ padding: 4px 11px;
+ border-radius: 999px;
+ background: var(--vscode-editorWidget-background);
+ color: var(--vscode-descriptionForeground);
+ border: 1px solid var(--vscode-panel-border);
+ }
+ .pill .dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ flex: none;
+ background: var(--vscode-editorLineNumber-foreground);
+ }
+ .pill.passed .dot {
+ background: var(--vscode-charts-green);
}
- .step-cell.divergent {
- background: rgba(255, 90, 90, 0.08);
+ .pill.failed {
+ color: var(--vscode-charts-red);
+ border-color: color-mix(in srgb, var(--vscode-charts-red) 35%, transparent);
}
- .step-cell.divergent.first {
- background: rgba(255, 90, 90, 0.18);
- border-left: 3px solid var(--vscode-charts-red, #f48771);
+ .pill.failed .dot {
+ background: var(--vscode-charts-red);
}
- .marker {
- margin-left: 0.35rem;
- font-size: 0.85em;
+ .swap-ico {
+ color: var(--vscode-editorLineNumber-foreground);
}
- .marker.result {
- color: var(--vscode-charts-orange, #d19a66);
+ .scope {
+ font-size: 11px;
+ color: var(--vscode-editorLineNumber-foreground);
}
- .marker.error {
- color: var(--vscode-charts-red, #f48771);
+ .actions-group {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
- .marker.command {
- color: var(--vscode-charts-red, #f48771);
+ .toggle-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ font-size: 11.5px;
+ color: var(--vscode-descriptionForeground);
}
- .marker.ok {
- color: var(--vscode-charts-green, #73c373);
+ button.action {
+ background: var(--vscode-editorWidget-background);
+ border: 1px solid var(--vscode-panel-border);
+ color: var(--vscode-descriptionForeground);
+ font-size: 11.5px;
+ padding: 5px 12px;
+ border-radius: 7px;
+ cursor: pointer;
+ font-family: inherit;
}
- .marker.info {
- color: var(--vscode-descriptionForeground, #999);
- opacity: 0.7;
+ button.action:hover {
+ color: var(--vscode-foreground);
+ background: var(--vscode-list-hoverBackground);
+ }
+ button.action.icon-only {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 5px 8px;
+ }
+ button.action.icon-only svg {
+ width: 1em;
+ height: 1em;
}
+
+ /* ββ Error banner ββ */
.error-banner {
- margin: 0.5rem 0.75rem;
- padding: 0.5rem 0.75rem;
- background: rgba(244, 135, 113, 0.12);
- border-left: 3px solid var(--vscode-charts-red, #f48771);
- border-radius: 3px;
- font-size: 0.85em;
+ flex: none;
+ margin: 12px 14px 0;
+ padding: 10px 14px;
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--vscode-charts-red) 9%, transparent);
+ border: 1px solid
+ color-mix(in srgb, var(--vscode-charts-red) 25%, transparent);
}
.error-banner-title {
- font-weight: 600;
- margin-bottom: 0.25rem;
- opacity: 0.85;
- font-family: inherit;
+ font-size: 10px;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: var(--vscode-charts-red);
+ margin-bottom: 5px;
}
/* Pre-wrap only on the message body so template indentation doesn't render. */
.error-banner-message {
font-family: var(--vscode-editor-font-family, monospace);
+ font-size: 12px;
+ color: var(--vscode-foreground);
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
- .step-cell.missing {
- opacity: 0.35;
- font-style: italic;
+
+ /* ββ Diff body ββ */
+ /* Header sits OUTSIDE the scroll container so rows can't scroll above it. */
+ .cmp-colhead {
+ flex: none;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ padding: 10px 14px 8px;
+ background: var(--vscode-editor-background);
+ border-bottom: 1px solid var(--vscode-panel-border);
+ }
+ .cmp-body {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ padding: 12px 14px;
+ }
+ .col-header {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ color: var(--vscode-foreground);
+ }
+ .cmp-rows {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+ .step-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ }
+ .step-cell {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 8px 12px;
+ border-radius: 8px;
+ font-family: var(--vscode-editor-font-family, monospace);
+ font-size: 12.5px;
+ color: var(--vscode-foreground);
+ background: var(--vscode-editorWidget-background);
+ border: 1px solid var(--vscode-panel-border);
+ cursor: pointer;
+ }
+ .step-cell code {
+ font-family: inherit;
}
.step-cell:hover {
- background: var(
- --vscode-toolbar-hoverBackground,
- rgba(255, 255, 255, 0.06)
- );
+ background: var(--vscode-list-hoverBackground);
+ }
+ .step-cell.divergent {
+ background: color-mix(in srgb, var(--vscode-charts-red) 9%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-charts-red) 30%, transparent);
+ box-shadow: inset 3px 0 0 var(--vscode-charts-red);
+ }
+ .step-cell.divergent.first {
+ background: color-mix(in srgb, var(--vscode-charts-red) 16%, transparent);
}
.step-cell.expanded {
- background: rgba(80, 160, 255, 0.06);
+ outline: 1px solid var(--accent);
}
- .pill {
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- padding: 0.1rem 0.5rem;
- border-radius: 4px;
- font-size: 0.85em;
- background: var(--vscode-badge-background, #2a2a2a);
+ .step-cell.missing {
+ justify-content: center;
+ color: var(--vscode-editorLineNumber-foreground);
+ background: transparent;
+ border-style: dashed;
+ cursor: default;
+ font-style: italic;
}
- .pill.failed {
- background: rgba(244, 135, 113, 0.2);
- color: var(--vscode-charts-red, #f48771);
+
+ /* ββ Per-cell status markers ββ */
+ /* Only the first marker pushes the group to the right edge; any following
+ marker (e.g. the β after an "only here" tag) sits adjacent, so the β
+ stays in the same right-edge column across rows. */
+ .marker {
+ margin-left: 8px;
+ font-family: var(--vscode-font-family, sans-serif);
+ font-size: 10px;
+ padding: 1px 7px;
+ border-radius: 5px;
}
- .pill.passed {
- background: rgba(115, 195, 115, 0.2);
- color: var(--vscode-charts-green, #73c373);
+ .step-cell .marker:first-of-type {
+ margin-left: auto;
}
- .topbar {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 0.75rem;
- border-bottom: 1px solid var(--vscode-panel-border, #2a2a2a);
- flex: 0 0 auto;
+ .marker.ok {
+ padding: 0;
+ background: transparent;
+ font-size: 12px;
+ color: var(--vscode-charts-green);
}
- .col-header {
- position: sticky;
- top: 0;
- background: var(--vscode-editor-background, #1e1e1e);
- z-index: 1;
- padding: 0.5rem;
- font-weight: 600;
- font-size: 0.85em;
- border-bottom: 1px solid var(--vscode-panel-border, #2a2a2a);
+ .marker.command,
+ .marker.error {
+ color: var(--vscode-charts-red);
+ background: color-mix(in srgb, var(--vscode-charts-red) 16%, transparent);
+ }
+ .marker.result {
+ color: var(--vscode-charts-yellow);
+ background: color-mix(
+ in srgb,
+ var(--vscode-charts-yellow) 16%,
+ transparent
+ );
}
+ .marker.info {
+ color: var(--vscode-charts-yellow);
+ background: color-mix(
+ in srgb,
+ var(--vscode-charts-yellow) 16%,
+ transparent
+ );
+ }
+
+ /* ββ Expanded row detail ββ */
.detail-panel {
- grid-column: span 2;
- background: var(--vscode-editor-background, #1e1e1e);
- border-bottom: 1px solid var(--vscode-panel-border, #2a2a2a);
- padding: 0.5rem;
+ grid-column: 1 / -1;
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: var(--vscode-editorWidget-background);
+ border: 1px solid var(--vscode-panel-border);
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 0.75rem;
+ gap: 14px;
}
.detail-block {
- font-size: 0.85em;
+ font-size: 12px;
}
.detail-block h4 {
- font-size: 0.85em;
- margin: 0 0 0.25rem;
- opacity: 0.7;
- font-weight: 600;
+ font-size: 10px;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+ font-weight: 700;
+ margin: 0 0 6px;
+ color: var(--vscode-editorLineNumber-foreground);
}
.detail-block pre {
margin: 0;
+ padding: 8px 10px;
+ border-radius: 6px;
+ font-size: 11px;
white-space: pre-wrap;
word-break: break-word;
- font-size: 0.85em;
- background: rgba(255, 255, 255, 0.03);
- padding: 0.25rem 0.4rem;
- border-radius: 3px;
+ color: var(--vscode-descriptionForeground);
+ background: var(--vscode-editor-background);
+ border: 1px solid var(--vscode-panel-border);
}
+
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
- color: var(--vscode-descriptionForeground, #888);
- font-size: 0.9em;
- text-align: center;
padding: 1rem;
- }
- .toggle-label {
- display: inline-flex;
- align-items: center;
- gap: 0.35rem;
- cursor: pointer;
- font-size: 0.85em;
- }
- button.action {
- background: transparent;
- border: 1px solid var(--vscode-panel-border, #2a2a2a);
- color: inherit;
- padding: 0.2rem 0.5rem;
- border-radius: 3px;
- cursor: pointer;
- font-size: 0.85em;
- }
- button.action:hover {
- background: var(
- --vscode-toolbar-hoverBackground,
- rgba(255, 255, 255, 0.06)
- );
- }
- button.action.icon-only {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0.25rem 0.4rem;
- }
- button.action.icon-only svg {
- width: 1em;
- height: 1em;
+ text-align: center;
+ font-size: 0.9em;
+ color: var(--vscode-descriptionForeground, #888);
}
`
diff --git a/packages/app/src/components/workbench/console-filter.ts b/packages/app/src/components/workbench/console-filter.ts
new file mode 100644
index 00000000..2a325ef0
--- /dev/null
+++ b/packages/app/src/components/workbench/console-filter.ts
@@ -0,0 +1,67 @@
+/**
+ * Pure helpers for the Console tab's search + level filtering, split out so the
+ * matching logic can be unit-tested without rendering the component.
+ */
+
+/** Level filter options β `all` plus one per captured log type. */
+export type ConsoleLevelFilter = 'all' | 'error' | 'warn' | 'info' | 'log'
+
+/** Ordered level filters for the Console toolbar: filter key + display label. */
+export const CONSOLE_LEVEL_FILTERS: ReadonlyArray<{
+ key: ConsoleLevelFilter
+ label: string
+}> = [
+ { key: 'all', label: 'All' },
+ { key: 'error', label: 'Errors' },
+ { key: 'warn', label: 'Warnings' },
+ { key: 'info', label: 'Info' },
+ { key: 'log', label: 'Logs' }
+]
+
+// SGR escape sequences (e.g. `[31m`) from the WDIO terminal logger render
+// as stray `[31m` once the invisible ESC is dropped β strip them for display.
+const ANSI_SGR_RE = /\[[0-9;]*m/g
+
+/** Remove terminal ANSI color codes so logger output reads cleanly in the UI. */
+export function stripAnsi(value: string): string {
+ return value.replace(ANSI_SGR_RE, '')
+}
+
+/** Render a log entry's args into one string for display and search. */
+export function formatConsoleArgs(args: unknown): string {
+ if (!Array.isArray(args)) {
+ return stripAnsi(String(args))
+ }
+ return stripAnsi(
+ args
+ .map((arg) => {
+ if (typeof arg === 'string') {
+ return arg
+ }
+ try {
+ return JSON.stringify(arg, null, 2)
+ } catch {
+ return String(arg)
+ }
+ })
+ .join(' ')
+ )
+}
+
+/** Filter logs by level and a case-insensitive substring of the message. */
+export function filterConsoleLogs(
+ logs: ConsoleLogs[],
+ level: ConsoleLevelFilter,
+ search: string
+): ConsoleLogs[] {
+ const needle = search.trim().toLowerCase()
+ return logs.filter((log) => {
+ if (level !== 'all' && (log.type || 'log') !== level) {
+ return false
+ }
+ if (needle && !formatConsoleArgs(log.args).toLowerCase().includes(needle)) {
+ return false
+ }
+ return true
+ })
+}
diff --git a/packages/app/src/components/workbench/console.ts b/packages/app/src/components/workbench/console.ts
index 09d451a6..ae986562 100644
--- a/packages/app/src/components/workbench/console.ts
+++ b/packages/app/src/components/workbench/console.ts
@@ -1,10 +1,16 @@
import { Element } from '@core/element'
-import { html, css, nothing } from 'lit'
-import { customElement } from 'lit/decorators.js'
+import { html, css } from 'lit'
+import { customElement, state } from 'lit/decorators.js'
import { consume } from '@lit/context'
import { consoleLogContext } from '../../controller/context.js'
-import { LOG_ICONS } from '../../controller/constants.js'
+import { LOG_ICONS, CONSOLE_SOURCE_BADGE } from '../../controller/constants.js'
+import {
+ filterConsoleLogs,
+ formatConsoleArgs,
+ CONSOLE_LEVEL_FILTERS,
+ type ConsoleLevelFilter
+} from './console-filter.js'
const SOURCE_COMPONENT = 'wdio-devtools-console-logs'
@customElement(SOURCE_COMPONENT)
@@ -21,89 +27,148 @@ export class DevtoolsConsoleLogs extends Element {
position: relative;
}
+ /* ββ Toolbar: filter input + segmented level filter ββ */
+ .console-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ flex-shrink: 0;
+ }
+ .search-input {
+ flex: 1;
+ max-width: 280px;
+ padding: 6px 10px;
+ border: 1px solid var(--vscode-panel-border);
+ background: var(--vscode-input-background);
+ color: var(--vscode-foreground);
+ border-radius: 8px;
+ font-size: 12px;
+ }
+ .search-input::placeholder {
+ color: var(--vscode-descriptionForeground);
+ }
+ .search-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ }
+ .filter-tabs {
+ display: flex;
+ gap: 2px;
+ padding: 2px;
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 8px;
+ background: var(--vscode-input-background);
+ }
+ .filter-tab {
+ border: none;
+ background: transparent;
+ color: var(--vscode-descriptionForeground);
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 4px 10px;
+ border-radius: 6px;
+ transition:
+ background-color 0.15s ease,
+ color 0.15s ease;
+ }
+ .filter-tab:hover {
+ color: var(--vscode-foreground);
+ }
+ .filter-tab.active {
+ background: var(--accent);
+ color: var(--accent-foreground);
+ }
+
+ /* ββ Log list ββ */
.console-container {
flex: 1;
overflow-y: auto;
- font-family:
- 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
- 'Courier New', monospace;
- font-size: 13px;
- line-height: 1.6;
+ font-family: var(--vscode-editor-font-family);
+ font-size: 12px;
+ padding: 4px 0;
}
.log-entry {
- display: flex;
- align-items: flex-start;
- padding: 2px 8px;
- border-bottom: 1px solid rgba(128, 128, 128, 0.05);
- min-height: 22px;
+ display: grid;
+ grid-template-columns: 46px 16px auto 1fr;
+ align-items: start;
+ gap: 10px;
+ padding: 4px 14px;
+ border-bottom: 1px solid var(--vscode-panel-border);
+ line-height: 1.55;
}
-
.log-entry:hover {
- background-color: rgba(255, 255, 255, 0.02);
+ background-color: var(--vscode-list-hoverBackground);
}
-
.log-entry.log-type-error {
- background-color: rgba(244, 135, 113, 0.03);
+ background-color: color-mix(
+ in srgb,
+ var(--vscode-charts-red) 6%,
+ transparent
+ );
}
-
.log-entry.log-type-warn {
- background-color: rgba(205, 151, 49, 0.03);
- }
-
- .log-entry.log-type-info {
- background-color: rgba(14, 99, 156, 0.03);
- }
-
- .log-time,
- .log-icon {
- flex-shrink: 0;
+ background-color: color-mix(
+ in srgb,
+ var(--vscode-charts-yellow) 6%,
+ transparent
+ );
}
.log-time {
- width: 45px;
text-align: right;
- margin-right: 12px;
- font-size: 11px;
- opacity: 0.5;
- user-select: none;
+ font-size: 10.5px;
color: var(--vscode-editorLineNumber-foreground);
- line-height: 18px;
+ font-variant-numeric: tabular-nums;
+ user-select: none;
}
-
.log-icon {
- margin-right: 8px;
- font-size: 14px;
- line-height: 18px;
+ text-align: center;
+ line-height: 1.55;
+ color: var(--vscode-descriptionForeground);
}
-
- .log-prefix {
- flex-shrink: 0;
- color: var(--vscode-foreground);
- opacity: 0.8;
- margin-right: 4px;
- font-weight: 600;
+ .log-entry.log-type-error .log-icon,
+ .log-entry.log-type-error .log-message {
+ color: var(--vscode-charts-red);
}
-
- .log-prefix.source-test {
- color: #4ec9b0;
+ .log-entry.log-type-warn .log-icon,
+ .log-entry.log-type-warn .log-message {
+ color: var(--vscode-charts-yellow);
}
-
- .log-prefix.source-terminal {
- color: #ce9178;
+ .log-entry.log-type-info .log-icon {
+ color: var(--vscode-charts-blue);
}
- .log-prefix.source-browser {
- color: #569cd6;
+ .log-badge {
+ justify-self: start;
+ font-size: 9.5px;
+ line-height: 1.55;
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ padding: 2px 6px;
+ border-radius: 5px;
}
-
- .log-content {
- flex: 1;
- min-width: 0;
- word-break: break-word;
- line-height: 18px;
- display: flex;
- align-items: baseline;
+ .b-test {
+ color: var(--vscode-charts-green);
+ background: color-mix(
+ in srgb,
+ var(--vscode-charts-green) 14%,
+ transparent
+ );
+ }
+ .b-runner {
+ color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
+ }
+ .b-browser {
+ color: var(--vscode-charts-blue);
+ background: color-mix(
+ in srgb,
+ var(--vscode-charts-blue) 14%,
+ transparent
+ );
}
.log-message {
@@ -112,18 +177,6 @@ export class DevtoolsConsoleLogs extends Element {
word-break: break-word;
}
- .log-entry.log-type-error .log-message {
- color: #f48771;
- }
-
- .log-entry.log-type-warn .log-message {
- color: #cd9731;
- }
-
- .log-entry.log-type-info .log-message {
- color: #75beff;
- }
-
.empty-state {
position: absolute;
inset: 0;
@@ -134,12 +187,10 @@ export class DevtoolsConsoleLogs extends Element {
gap: 12px;
color: var(--vscode-descriptionForeground);
}
-
.empty-state-icon {
font-size: 48px;
opacity: 0.3;
}
-
.empty-state-text {
font-size: 14px;
opacity: 0.6;
@@ -150,9 +201,11 @@ export class DevtoolsConsoleLogs extends Element {
@consume({ context: consoleLogContext, subscribe: true })
logs: ConsoleLogs[] | undefined = undefined
- get logCount(): number {
- return this.logs?.length || 0
- }
+ @state()
+ private searchText = ''
+
+ @state()
+ private activeLevel: ConsoleLevelFilter = 'all'
#startTime?: number
@@ -164,22 +217,34 @@ export class DevtoolsConsoleLogs extends Element {
return `${elapsed.toFixed(1)}s`
}
- #formatArgs(args: unknown): string {
- if (Array.isArray(args)) {
- return args
- .map((arg) => {
- if (typeof arg === 'string') {
- return arg
- }
- try {
- return JSON.stringify(arg, null, 2)
- } catch {
- return String(arg)
- }
- })
- .join(' ')
- }
- return String(args)
+ #renderToolbar() {
+ return html`
+
+ `
}
#renderEmptyState() {
@@ -193,31 +258,17 @@ export class DevtoolsConsoleLogs extends Element {
#renderLogEntry(log: ConsoleLogs) {
const icon = LOG_ICONS[log.type] || LOG_ICONS.log
- const sourceLabel =
- log.source === 'test'
- ? '[TEST]'
- : log.source === 'terminal'
- ? '[WDIO]'
- : log.source === 'browser'
- ? '[BROWSER]'
- : ''
- const sourceClass = log.source ? `source-${log.source}` : ''
+ const badge = log.source ? CONSOLE_SOURCE_BADGE[log.source] : undefined
return html`
- ${log.timestamp
- ? html`
- ${this.#formatElapsedTime(log.timestamp)}
-
`
- : nothing}
-
${icon}
-
- ${sourceLabel
- ? html`
${sourceLabel} `
- : nothing}
-
${this.#formatArgs(log.args)}
+
+ ${log.timestamp ? this.#formatElapsedTime(log.timestamp) : ''}
+
${icon}
+ ${badge
+ ? html`
${badge.label} `
+ : html`
`}
+
${formatConsoleArgs(log.args)}
`
}
@@ -226,9 +277,19 @@ export class DevtoolsConsoleLogs extends Element {
if (!this.logs || this.logs.length === 0) {
return this.#renderEmptyState()
}
+ const visible = filterConsoleLogs(
+ this.logs,
+ this.activeLevel,
+ this.searchText
+ )
return html`
+ ${this.#renderToolbar()}
- ${this.logs.map((log) => this.#renderLogEntry(log))}
+ ${visible.length
+ ? visible.map((log) => this.#renderLogEntry(log))
+ : html`
+ No logs match the current filter.
+
`}
`
}
diff --git a/packages/app/src/components/workbench/list.ts b/packages/app/src/components/workbench/list.ts
deleted file mode 100644
index 5fc0b995..00000000
--- a/packages/app/src/components/workbench/list.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { Element } from '@core/element'
-import { html, css } from 'lit'
-import { customElement, property } from 'lit/decorators.js'
-
-import '~icons/mdi/chevron-right.js'
-
-const SOURCE_COMPONENT = 'wdio-devtools-list'
-@customElement(SOURCE_COMPONENT)
-export class DevtoolsList extends Element {
- @property({ type: Boolean })
- isCollapsed = false
-
- @property({ type: String })
- label = ''
-
- @property({ type: Object })
- list: Record
| unknown[] = {}
-
- static styles = [
- ...Element.styles,
- css`
- :host {
- display: block;
- width: 100%;
- }
- dl {
- width: 100%;
- }
- dt {
- font-weight: 600;
- font-size: 11px;
- letter-spacing: 0.5px;
- text-transform: uppercase;
- opacity: 0.75;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- pre {
- margin: 0;
- font-family: var(--vscode-editor-font-family, monospace);
- font-size: 11px;
- line-height: 1.25;
- white-space: pre-wrap;
- word-break: break-word;
- overflow-wrap: anywhere;
- background: var(--vscode-editorInlayHint-background, transparent);
- padding: 2px 4px;
- border-radius: 3px;
- max-height: 16rem;
- overflow: auto;
- }
- .row {
- transition: max-height 0.18s ease;
- box-sizing: border-box;
- }
- .collapse {
- max-height: 0 !important;
- overflow: hidden !important;
- padding-top: 0 !important;
- padding-bottom: 0 !important;
- border-bottom-width: 0 !important;
- }
- `
- ]
-
- #renderMetadataProp(prop: unknown) {
- if (typeof prop === 'object' && prop !== null) {
- return html`${JSON.stringify(prop, null, 2)} `
- }
- return html`${String(prop)} `
- }
-
- #toggleCollapseState() {
- this.isCollapsed = !this.isCollapsed
- this.requestUpdate()
- }
-
- #renderSectionHeader(label: string) {
- return html`
- this.#toggleCollapseState()}
- class="block w-full border-b-[1px] border-b-panelBorder font-bold flex py-1 px-1"
- >
-
- ${label}
-
- `
- }
-
- #unpackEntry(
- entry: unknown,
- isArrayList: boolean
- ): { key: string | undefined; val: unknown } {
- const isKeyValueTuple = (v: unknown): v is [string, unknown] =>
- Array.isArray(v) && v.length === 2 && typeof v[0] === 'string'
- if (isArrayList) {
- if (isKeyValueTuple(entry)) {
- return { key: entry[0], val: entry[1] }
- }
- return { key: undefined, val: entry }
- }
- const tuple = entry as [string, unknown]
- return { key: tuple[0], val: tuple[1] }
- }
-
- #renderRow(entry: unknown, isArrayList: boolean) {
- const { key, val } = this.#unpackEntry(entry, isArrayList)
- const stringForMeasure =
- val && typeof val === 'object'
- ? JSON.stringify(val, null, 2)
- : String(val)
- const isMultiline =
- /\n/.test(stringForMeasure) ||
- stringForMeasure.length > 40 ||
- (val && typeof val === 'object')
- const baseCls = 'row px-2 py-1 border-b-[1px] border-b-panelBorder'
- const colCls = isMultiline ? 'basis-full w-full' : 'basis-1/2'
- const collapsedCls = this.isCollapsed ? 'collapse' : 'max-h-[500px]'
- if (key === undefined) {
- return html`
-
- ${this.#renderMetadataProp(val)}
-
- `
- }
- return html`
- ${key}
-
- ${this.#renderMetadataProp(val)}
-
- `
- }
-
- render() {
- const list = this.list ?? {}
- const isArrayList = Array.isArray(list)
- if (list === null) {
- return null
- }
- if (isArrayList && (list as unknown[]).length === 0) {
- return null
- }
- if (
- !isArrayList &&
- Object.keys(list as Record).length === 0
- ) {
- return null
- }
- const entries: unknown[] = isArrayList
- ? (this.list as unknown[])
- : Object.entries(this.list as Record)
- return html`
-
- ${this.#renderSectionHeader(this.label)}
-
- ${entries.map((entry) => this.#renderRow(entry, isArrayList))}
-
-
- `
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- [SOURCE_COMPONENT]: DevtoolsList
- }
-}
diff --git a/packages/app/src/components/workbench/logs.ts b/packages/app/src/components/workbench/logs.ts
index 674082ce..2c065283 100644
--- a/packages/app/src/components/workbench/logs.ts
+++ b/packages/app/src/components/workbench/logs.ts
@@ -1,11 +1,12 @@
import { Element } from '@core/element'
-import { html, css } from 'lit'
+import { html, css, nothing, type TemplateResult } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import type { CommandLog } from '@wdio/devtools-shared'
import type { CommandEndpoint } from '@wdio/protocols'
-import './list.js'
+import { commandCategory } from './actionItems/category.js'
+import { formatDuration } from './actionItems/duration.js'
const SOURCE_COMPONENT = 'wdio-devtools-logs'
@customElement(SOURCE_COMPONENT)
@@ -22,10 +23,122 @@ export class DevtoolsCommandLogs extends Element {
...Element.styles,
css`
:host {
- display: block;
+ display: flex;
+ flex-direction: column;
width: 100%;
height: 100%;
- min-height: 200px;
+ min-height: 0;
+ color: var(--vscode-foreground);
+ }
+
+ .cmd-empty {
+ flex: 1;
+ display: grid;
+ place-items: center;
+ color: var(--vscode-descriptionForeground);
+ font-size: 13px;
+ }
+
+ .cmd-head {
+ flex: none;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--vscode-panel-border);
+ }
+ .cat-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 3px;
+ flex: none;
+ }
+ .cat-navigation {
+ background: var(--vscode-charts-blue);
+ }
+ .cat-input {
+ background: var(--vscode-charts-purple);
+ }
+ .cat-assertion {
+ background: var(--vscode-charts-green);
+ }
+ .cat-query {
+ background: var(--vscode-charts-yellow);
+ }
+ .cat-other {
+ background: var(--vscode-descriptionForeground);
+ }
+ .cmd-name {
+ font-family: var(--vscode-editor-font-family);
+ font-weight: 700;
+ font-size: 14px;
+ }
+ .cmd-dur {
+ font-family: var(--vscode-editor-font-family);
+ font-size: 11px;
+ color: var(--vscode-editorLineNumber-foreground);
+ }
+ .cmd-ref {
+ margin-left: auto;
+ font-size: 12px;
+ color: var(--accent);
+ text-decoration: none;
+ }
+ .cmd-ref:hover {
+ text-decoration: underline;
+ }
+
+ .cmd-body {
+ flex: 1;
+ overflow: auto;
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ .dsec > h4 {
+ font-size: 10.5px;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+ color: var(--vscode-descriptionForeground);
+ margin-bottom: 8px;
+ }
+ .cmd-desc {
+ font-size: 12.5px;
+ color: var(--vscode-descriptionForeground);
+ line-height: 1.65;
+ }
+
+ .kv-card {
+ background: var(--vscode-editorWidget-background);
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 10px;
+ overflow: hidden;
+ }
+ .kv {
+ display: grid;
+ grid-template-columns: minmax(80px, auto) 1fr;
+ gap: 14px;
+ padding: 9px 14px;
+ font-size: 12px;
+ border-top: 1px solid var(--vscode-panel-border);
+ }
+ .kv:first-child {
+ border-top: none;
+ }
+ .kv .k {
+ color: var(--vscode-descriptionForeground);
+ font-family: var(--vscode-editor-font-family);
+ white-space: nowrap;
+ }
+ .kv .v {
+ color: var(--vscode-foreground);
+ font-family: var(--vscode-editor-font-family);
+ word-break: break-all;
+ text-align: right;
+ }
+ .kv .v.empty {
+ color: var(--vscode-editorLineNumber-foreground);
}
`
]
@@ -67,75 +180,103 @@ export class DevtoolsCommandLogs extends Element {
this.#commandDefinition = endpoints[command.command]
this.command = command
- window.dispatchEvent(
- new CustomEvent('app-source-highlight', {
- detail: this.command?.callSource
- })
- )
+ // Source line-tracking is dispatched by the Actions handler; here we only
+ // surface the command's detail in the Log tab.
this.closest('wdio-devtools-tabs')?.activateTab('Log')
})
}
+ #stringify(value: unknown): string {
+ if (typeof value === 'string') {
+ return value
+ }
+ try {
+ return JSON.stringify(value, null, 2)
+ } catch {
+ return String(value)
+ }
+ }
+
+ #renderKvCard(rows: Array<[string, unknown]>): TemplateResult {
+ return html`
+
+ ${rows.map(([key, value]) => {
+ const isEmpty = value === null || value === undefined
+ return html`
+ ${key}
+ ${isEmpty ? 'null' : this.#stringify(value)}
+
`
+ })}
+
+ `
+ }
+
#renderParameters() {
const args = this.command!.args || []
- const params = args.reduce(
- (acc: Record, val: unknown, i: number) => {
- const paramName = this.#commandDefinition?.parameters?.[i]?.name ?? i
- acc[paramName] = val
- return acc
- },
- {} as Record
- )
- return html` `
+ if (args.length === 0) {
+ return nothing
+ }
+ const rows = args.map((val, i): [string, unknown] => [
+ String(this.#commandDefinition?.parameters?.[i]?.name ?? i),
+ val
+ ])
+ return html`
+
+
Parameters
+ ${this.#renderKvCard(rows)}
+
+ `
}
#renderResult() {
const result = this.command!.result
if (result === null || result === undefined) {
- return ''
+ return nothing
}
- return html` `
+ const rows: Array<[string, unknown]> =
+ typeof result === 'object' ? Object.entries(result) : [['value', result]]
+ return html`
+
+
Result
+ ${this.#renderKvCard(rows)}
+
+ `
}
render() {
if (!this.command) {
- return html`
-
- Please select a command to view details!
-
- `
+ return html`
+ Select a command to view its details
+
`
}
+ const category = commandCategory(this.command.command)
+ const definition = this.#commandDefinition
return html`
-
- ${this.command.command}
- ${this.#commandDefinition &&
- html`Reference `}
-
- ${this.#commandDefinition &&
- html`
-
-
- `}
- ${this.#renderParameters()} ${this.#renderResult()}
+
+
+
${this.command.command}
+ ${this.elapsedTime !== undefined
+ ? html`
${formatDuration(this.elapsedTime)} `
+ : nothing}
+ ${definition
+ ? html`
Reference β `
+ : nothing}
+
+
+ ${definition?.description
+ ? html`
+
Description
+
${definition.description}
+
`
+ : nothing}
+ ${this.#renderParameters()} ${this.#renderResult()}
+
`
}
}
diff --git a/packages/app/src/components/workbench/metadata.ts b/packages/app/src/components/workbench/metadata.ts
index 75a0553f..0bae239d 100644
--- a/packages/app/src/components/workbench/metadata.ts
+++ b/packages/app/src/components/workbench/metadata.ts
@@ -1,21 +1,36 @@
import { Element } from '@core/element'
-import { html, css } from 'lit'
-import { customElement } from 'lit/decorators.js'
+import { html, css, nothing, type TemplateResult } from 'lit'
+import { customElement, state } from 'lit/decorators.js'
import { consume } from '@lit/context'
-import type { Metadata } from '@wdio/devtools-shared'
-import { metadataContext } from '../../controller/context.js'
+import type { Metadata, MetadataBySession } from '@wdio/devtools-shared'
+import {
+ metadataContext,
+ metadataBySessionContext
+} from '../../controller/context.js'
+import { PENDING_SESSION_KEY } from '../../controller/contextUpdates.js'
-import './list.js'
import '../placeholder.js'
import '~icons/mdi/chevron-right.js'
const SOURCE_COMPONENT = 'wdio-devtools-metadata'
@customElement(SOURCE_COMPONENT)
export class DevtoolsMetadata extends Element {
+ /** Latest/active session metadata β fallback when no per-session map exists
+ * (e.g. a loaded single-session trace without a sessionId). */
@consume({ context: metadataContext, subscribe: true })
metadata: Partial | undefined = undefined
+ @consume({ context: metadataBySessionContext, subscribe: true })
+ metadataBySession: MetadataBySession | undefined = undefined
+
+ /** sessionId the user picked in the dropdown; falls back to the latest. */
+ @state()
+ private selectedSessionId?: string
+
+ /** Section labels the user has collapsed. */
+ #collapsed = new Set()
+
static styles = [
...Element.styles,
css`
@@ -24,12 +39,117 @@ export class DevtoolsMetadata extends Element {
flex-direction: column;
width: 100%;
height: 100%;
- font-size: 0.8em;
+ overflow: auto;
+ }
+
+ .meta {
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .session-select {
+ align-self: flex-start;
+ font-size: 11px;
+ font-family: inherit;
+ padding: 5px 8px;
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 8px;
+ background: var(--vscode-input-background);
+ color: var(--vscode-foreground);
+ cursor: pointer;
+ line-height: 1;
+ }
+
+ .meta-sec h4 {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin: 0 0 8px;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--vscode-foreground);
+ cursor: pointer;
+ user-select: none;
+ }
+ .meta-sec .chev {
+ color: var(--vscode-descriptionForeground);
+ transition: transform 0.15s;
+ }
+ .meta-sec .chev.open {
+ transform: rotate(90deg);
+ }
+
+ .meta-card {
+ background: var(--vscode-editorWidget-background);
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 10px;
+ overflow: hidden;
+ }
+
+ .mrow {
+ display: grid;
+ grid-template-columns: minmax(120px, 200px) 1fr;
+ gap: 14px;
+ padding: 9px 14px;
+ font-size: 12px;
+ border-top: 1px solid var(--vscode-panel-border);
+ }
+ .mrow:first-child {
+ border-top: none;
+ }
+ .mrow .k {
+ color: var(--vscode-descriptionForeground);
+ font-family: var(--vscode-editor-font-family);
+ font-size: 11px;
+ word-break: break-word;
+ }
+ .mrow .v {
+ color: var(--vscode-foreground);
+ font-family: var(--vscode-editor-font-family);
+ word-break: break-all;
+ }
+ .mrow .v a {
+ color: var(--accent);
+ text-decoration: none;
+ }
+ .mrow .v a:hover {
+ text-decoration: underline;
+ }
+ .bool-true {
+ color: var(--vscode-charts-green);
+ }
+ .bool-false {
+ color: var(--vscode-charts-red);
+ }
+
+ /* Object/JSON values get a full-width recessed code block. */
+ .mrow.json {
+ display: block;
+ }
+ .mrow.json .k {
+ display: block;
+ margin-bottom: 8px;
+ }
+ .mrow.json pre {
+ margin: 0;
+ background: var(--vscode-editor-background);
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 8px;
+ padding: 10px 12px;
+ font-family: var(--vscode-editor-font-family);
+ font-size: 11px;
+ line-height: 1.5;
+ color: var(--vscode-descriptionForeground);
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
}
`
]
- #buildSessionInfo(m: MetadataShape): Record {
+ #buildSessionInfo(m: Metadata): Record {
const sessionInfo: Record = {}
if (m.sessionId) {
sessionInfo['Session ID'] = m.sessionId
@@ -49,46 +169,153 @@ export class DevtoolsMetadata extends Element {
return sessionInfo
}
- #renderListIfNonEmpty(label: string, list: Record) {
- return Object.keys(list).length
- ? html` `
- : ''
+ #toggle(label: string) {
+ if (this.#collapsed.has(label)) {
+ this.#collapsed.delete(label)
+ } else {
+ this.#collapsed.add(label)
+ }
+ this.requestUpdate()
+ }
+
+ #renderValue(val: unknown): TemplateResult | string {
+ if (typeof val === 'boolean') {
+ return html`${val} `
+ }
+ if (typeof val === 'string' && /^https?:\/\//.test(val)) {
+ return html`${val} `
+ }
+ return String(val)
+ }
+
+ #renderRow(key: string, val: unknown) {
+ if (val !== null && typeof val === 'object') {
+ return html`
+
+
${key}
+
${JSON.stringify(val, null, 2)}
+
+ `
+ }
+ return html`
+
+ ${key}
+ ${this.#renderValue(val)}
+
+ `
+ }
+
+ #renderSection(label: string, data: unknown) {
+ // Metadata's capability/option bags are typed `unknown` upstream; narrow to
+ // a record here so the section can iterate their key/value pairs.
+ const entries = Object.entries((data ?? {}) as Record)
+ if (entries.length === 0) {
+ return nothing
+ }
+ const open = !this.#collapsed.has(label)
+ return html`
+
+ `
+ }
+
+ /** Captured sessions in arrival order (the pending buffer is never shown). */
+ #sessions(): Array<[string, Metadata]> {
+ return Object.entries(this.metadataBySession ?? {}).filter(
+ ([id]) => id !== PENDING_SESSION_KEY
+ )
+ }
+
+ /** Map key of the session to display: the picked one (when still present),
+ * else the latest. Selection is by map key throughout β never the metadata's
+ * `sessionId` field β so the highlighted option and shown content can't drift. */
+ #activeKey(sessions: Array<[string, Metadata]>): string | undefined {
+ if (
+ this.selectedSessionId &&
+ sessions.some(([id]) => id === this.selectedSessionId)
+ ) {
+ return this.selectedSessionId
+ }
+ return sessions.length ? sessions[sessions.length - 1][0] : undefined
+ }
+
+ /** Metadata to display: the active session, else the single active value
+ * (loaded trace with no sessionId). */
+ #activeMetadata(sessions: Array<[string, Metadata]>): Metadata | undefined {
+ const key = this.#activeKey(sessions)
+ const found = key && sessions.find(([id]) => id === key)
+ return found ? found[1] : (this.metadata as Metadata | undefined)
+ }
+
+ /** Label a session by its index and (when known) its URL host, so the
+ * options are distinguishable β e.g. "Session 2 Β· www.google.com". */
+ #sessionLabel(meta: Metadata, index: number): string {
+ let host = ''
+ try {
+ if (meta.url) {
+ host = new URL(meta.url).host
+ }
+ } catch {
+ /* non-URL value β fall back to just the index */
+ }
+ return host ? `Session ${index + 1} Β· ${host}` : `Session ${index + 1}`
+ }
+
+ #renderSessionSelect(sessions: Array<[string, Metadata]>) {
+ if (sessions.length < 2) {
+ return nothing
+ }
+ const selectedKey = this.#activeKey(sessions)
+ return html`
+ {
+ this.selectedSessionId = (e.target as HTMLSelectElement).value
+ }}
+ >
+ ${sessions.map(
+ ([id, meta], i) =>
+ html`
+ ${this.#sessionLabel(meta, i)}
+ `
+ )}
+
+ `
}
render() {
- if (!this.metadata) {
+ const sessions = this.#sessions()
+ const active = this.#activeMetadata(sessions)
+ if (!active) {
return html` `
}
- const m = this.metadata as MetadataShape
return html`
- ${this.#renderListIfNonEmpty('Session', this.#buildSessionInfo(m))}
-
- ${this.#renderListIfNonEmpty(
- 'Desired Capabilities',
- m.desiredCapabilities || {}
- )}
- ${this.#renderListIfNonEmpty('Options', m.options || {})}
+
+ ${this.#renderSessionSelect(sessions)}
+ ${this.#renderSection('Session', this.#buildSessionInfo(active))}
+ ${this.#renderSection('Capabilities', active.capabilities)}
+ ${this.#renderSection(
+ 'Desired Capabilities',
+ active.desiredCapabilities
+ )}
+ ${this.#renderSection('Options', active.options)}
+
`
}
}
-interface MetadataShape {
- sessionId?: string
- testEnv?: string
- host?: string
- modulePath?: string
- url?: string
- capabilities?: Record
- desiredCapabilities?: Record
- options?: Record
-}
-
declare global {
interface HTMLElementTagNameMap {
[SOURCE_COMPONENT]: DevtoolsMetadata
diff --git a/packages/app/src/components/workbench/network.ts b/packages/app/src/components/workbench/network.ts
index 9fe9aa6b..e25c4b6e 100644
--- a/packages/app/src/components/workbench/network.ts
+++ b/packages/app/src/components/workbench/network.ts
@@ -4,13 +4,17 @@ import { networkStyles } from './network/styles.js'
import { customElement, state } from 'lit/decorators.js'
import { consume } from '@lit/context'
import { networkRequestContext } from '../../controller/context.js'
-import { RESOURCE_TYPES } from '../../utils/network-constants.js'
+import {
+ RESOURCE_TYPES,
+ TYPE_DOT_CLASS
+} from '../../utils/network-constants.js'
import {
formatBytes,
formatTime,
- getStatusClass,
+ statusKind,
getResourceType,
- getFileName
+ getFileName,
+ contentType
} from '../../utils/network-helpers.js'
import '../placeholder.js'
@@ -93,7 +97,9 @@ export class DevtoolsNetwork extends Element {
}
#selectRequest(request: NetworkRequest) {
- this.selectedRequest = request
+ // Clicking the already-selected row again closes the detail panel.
+ this.selectedRequest =
+ this.selectedRequest?.id === request.id ? undefined : request
}
#renderNetworkHeader() {
@@ -124,29 +130,31 @@ export class DevtoolsNetwork extends Element {
}
#renderRequestRow(request: NetworkRequest) {
+ const kind = statusKind(request.status, Boolean(request.error))
+ const dotClass = TYPE_DOT_CLASS[getResourceType(request)]
return html`
this.#selectRequest(request)}"
>
- ${getFileName(request.url)}
- ${request.method}
- ${request.status || (request.error ? 'ERR' : '-')}
- ${request.responseHeaders?.['content-type']?.split(';')[0] ||
- '-'}
- ${formatTime(request.time)}
- ${formatBytes(request.size)}
- ${request.startTime ? `${request.startTime.toFixed(1)}s` : '-'}
+
+ ${getFileName(request.url)}
+
+ ${request.method}
+
+
+ ${request.status || (request.error ? 'ERR' : 'β')}
+
+ ${contentType(request)}
+ ${formatTime(request.time)}
+ ${formatBytes(request.size)}
`
}
@@ -166,17 +174,16 @@ export class DevtoolsNetwork extends Element {
${this.#renderNetworkHeader()}