blob: 168496f91794ce7550b0f9af4fd79ee84e7fb1ac [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import sinon from 'sinon';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import {
getCleanTextContentFromElements,
renderElementIntoDOM,
} from '../../testing/DOMHelpers.js';
import {createTarget, describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {MockCDPConnection} from '../../testing/MockCDPConnection.js';
import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js';
import * as Application from './application.js';
describeWithEnvironment('ServiceWorkerCacheView', function() {
let target: SDK.Target.Target;
let cacheStorageModel: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel;
let cache: SDK.ServiceWorkerCacheModel.Cache;
// Use a third-party storage key (origin + top-level site) to ensure high-signal metadata rendering.
const testStorageKey = 'https://example.com/^0https://example.org';
const testStorageBucket: Protocol.Storage.StorageBucket = {
storageKey: testStorageKey,
};
beforeEach(() => {
const connection = new MockCDPConnection();
connection.setSuccessHandler('CacheStorage.requestEntries', () => ({cacheDataEntries: [], returnCount: 0}));
target = createTarget({connection});
cacheStorageModel = new SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel(target);
cache = new SDK.ServiceWorkerCacheModel.Cache(
cacheStorageModel, testStorageBucket, 'test-cache', 'id' as Protocol.CacheStorage.CacheId);
});
it('creates the expected view structure with toolbar, metadata, grid, and details pane', () => {
const view = new Application.ServiceWorkerCacheViews.ServiceWorkerCacheView(cacheStorageModel, cache);
const toolbar = view.element.querySelector('devtools-toolbar');
assert.isNotNull(toolbar, 'Expected a toolbar');
const metadataView = view.element.querySelector('devtools-storage-metadata-view');
assert.isNotNull(metadataView, 'Expected a metadata view');
const dataGrid = view.element.querySelector('.data-grid');
assert.isNotNull(dataGrid, 'Expected a cache entries data grid');
const detailsPane = view.element.querySelector('[slot="main"]');
assert.isNotNull(detailsPane, 'Expected a details pane');
});
it('renders metadata with storage key when no bucket info is found', async () => {
const view = new Application.ServiceWorkerCacheViews.ServiceWorkerCacheView(cacheStorageModel, cache);
renderElementIntoDOM(view);
const metadataView = view.element.querySelector('devtools-storage-metadata-view');
assert.isNotNull(metadataView);
await RenderCoordinator.done();
assert.isNotNull(metadataView.shadowRoot);
const keys = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-key');
assert.deepEqual(keys, ['Frame origin', 'Top-level site', 'Is third-party']);
const values = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-value');
assert.deepEqual(values, [
'https://example.com',
'https://example.org',
'Yes, because the origin is outside of the top-level site',
]);
view.detach();
});
it('renders metadata with storage bucket info when found', async () => {
const storageBucketsModel = target.model(SDK.StorageBucketsModel.StorageBucketsModel);
assert.isNotNull(storageBucketsModel);
sinon.stub(storageBucketsModel, 'getBucketByName').returns({
bucket: {storageKey: testStorageKey, name: 'test-bucket'},
id: 'id',
expiration: 42,
quota: 1024,
persistent: true,
durability: 'strict' as Protocol.Storage.StorageBucketsDurability,
});
const view = new Application.ServiceWorkerCacheViews.ServiceWorkerCacheView(cacheStorageModel, cache);
renderElementIntoDOM(view);
const metadataView = view.element.querySelector('devtools-storage-metadata-view');
assert.isNotNull(metadataView);
await RenderCoordinator.done();
assert.isNotNull(metadataView.shadowRoot);
const keys = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-key');
assert.deepEqual(keys, [
'Frame origin',
'Top-level site',
'Is third-party',
'Bucket name',
]);
const values = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-value');
assert.deepEqual(values, [
'https://example.com',
'https://example.org',
'Yes, because the origin is outside of the top-level site',
'test-bucket',
]);
view.detach();
});
it('updates when the cache is changed', async () => {
let resolveUpdate: () => void;
let updatedForTestPromise = new Promise<void>(resolve => {
resolveUpdate = resolve;
});
const loadStub =
sinon.stub(cacheStorageModel, 'loadAllCacheData').callsFake((_cache, _skipCount, callback) => callback([], 0));
const updateStub = sinon
.stub(Application.ServiceWorkerCacheViews.ServiceWorkerCacheView.prototype as unknown as
{updatedForTest: () => void},
'updatedForTest')
.callsFake(() => {
resolveUpdate();
});
const view = new Application.ServiceWorkerCacheViews.ServiceWorkerCacheView(cacheStorageModel, cache);
view.markAsRoot();
renderElementIntoDOM(view);
try {
await updatedForTestPromise;
sinon.assert.calledOnce(loadStub);
updatedForTestPromise = new Promise<void>(resolve => {
resolveUpdate = resolve;
});
cacheStorageModel.dispatchEventToListeners(SDK.ServiceWorkerCacheModel.Events.CACHE_STORAGE_CONTENT_UPDATED,
{cacheName: cache.cacheName, storageBucket: cache.storageBucket});
await updatedForTestPromise;
sinon.assert.calledTwice(loadStub);
} finally {
view.detach();
updateStub.restore();
}
});
});