Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.8.4] - 2026-04-01

### Fixed

- Loading skeleton was showing inside \<impact-popup> container on initial loads

## [2.8.3] - 2026-04-01

### Added
Expand Down Expand Up @@ -412,7 +418,8 @@ No release notes.

No release notes.

[unreleased]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.3...HEAD
[unreleased]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.4...HEAD
[2.8.4]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.2...@saasquatch%2Fsquatch-js@2.8.4
[2.8.3]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.2...@saasquatch%2Fsquatch-js@2.8.3
[2.8.2]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.1...@saasquatch%2Fsquatch-js@2.8.2
[2.8.1]: https://github.com/saasquatch/squatch-js/compare/@saasquatch%2Fsquatch-js@2.8.0...@saasquatch%2Fsquatch-js@2.8.1
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@saasquatch/squatch-js",
"type": "module",
"version": "2.8.3",
"version": "2.8.4",
"description": "The official Referral SaaSquatch Javascript Web/Browser SDK https://docs.referralsaasquatch.com/developer/squatchjs/",
"license": "MIT",
"author": "ReferralSaaSquatch.com, Inc.",
Expand Down
5 changes: 2 additions & 3 deletions src/widgets/declarative/DeclarativeWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,14 +349,13 @@ export default abstract class DeclarativeWidget extends HTMLElement {

const root = this.shadowRoot || this.attachShadow({ mode: "open" });

// For popup widgets, insert skeleton into the dialog container if it exists
// For popup widgets, only show skeleton inside an existing dialog (re-render case).
// On first render, skip skeleton since the popup isn't visible yet.
if (this.type === "POPUP") {
Comment on lines 350 to 354

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skeleton HTML is still being imported/built unconditionally, but in the POPUP first-render path it’s now never appended anywhere. Consider restructuring so the dynamic import + skeletonContainer creation only happens when you’re actually going to inject a skeleton (EMBED always, POPUP only when an existing dialog container is found) to avoid unnecessary async work on initial popup load.

Copilot uses AI. Check for mistakes.
const dialogContainer = root.getElementById("squatchModal");
if (dialogContainer) {
dialogContainer.innerHTML = "";
dialogContainer.appendChild(skeletonContainer);
} else {
root.appendChild(skeletonContainer);
}
} else {
root.innerHTML = "";
Expand Down
12 changes: 11 additions & 1 deletion test/specs/DeclarativeWidgets.feature
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,17 @@ Feature: Declarative widgets using custom Web Components
Examples:
| component |
| squatch-embed |
| squatch-popup |

@motivating @automated
Scenario: No loading skeleton is displayed for popup widgets before the popup is opened
Given window.squatchTenant is "valid"
And window.squatchToken is "a valid token"
And the squatch-popup web-component is included in the page's HTML
And the widget attribute is set to a valid SaaSquatch widget type
When the component starts loading
Then no loading skeleton is injected into the shadow DOM
When the widget finishes loading
Then no loading skeleton is present in the shadow DOM

@minutia @automated
Scenario Outline: Loading skeleton type is determined by the widget type
Expand Down
57 changes: 57 additions & 0 deletions test/step_definitions/DeclarativeWidgets.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,63 @@ defineFeature(feature, (test) => {
});
});

test("No loading skeleton is displayed for popup widgets before the popup is opened", ({
given,
and,
when,
then,
}) => {
let el!: DeclarativePopupWidget;
let renderPromiseResolve!: () => void;
let renderPromise!: Promise<void>;

Background(given);
SquatchTenantIs(given);
SquatchTokenIs(and);

and(/^the (.*) web-component is included in the page's HTML$/, (arg0) => {
el = specificWebComponentIsIncluded(arg0) as DeclarativePopupWidget;
});

and("the widget attribute is set to a valid SaaSquatch widget type", () => {
el.setAttribute("widget", "w/widget-type");
});

when("the component starts loading", () => {
renderPromise = new Promise((resolve) => {
renderPromiseResolve = resolve;
});
const originalRenderWidget = el.renderWidget.bind(el);
let proceedResolve: () => void;
const proceedPromise = new Promise<void>((r) => {
proceedResolve = r;
});
vi.spyOn(el, "renderWidget").mockImplementation(async () => {
await proceedPromise;
await originalRenderWidget();
renderPromiseResolve();
});
document.body.appendChild(el);
setTimeout(() => proceedResolve!(), 200);
});

then("no loading skeleton is injected into the shadow DOM", async () => {
// Give it time to potentially inject a skeleton, then confirm none exists
await new Promise((r) => setTimeout(r, 100));
const skeleton = el.shadowRoot?.getElementById("loading-skeleton");
Comment on lines +1385 to +1387

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion can be flaky because it relies on a fixed 100ms delay and assumes shadowRoot is already attached; if the dynamic import in connectedCallback takes longer, el.shadowRoot may still be null (making the expectation fail), and the test could also miss a skeleton injected after 100ms but before the render pause is released. Prefer waiting for shadowRoot to exist (e.g., via waitUntil) and then asserting the skeleton never appears during the whole paused-loading window (or using a MutationObserver) instead of a hard-coded sleep.

Suggested change
// Give it time to potentially inject a skeleton, then confirm none exists
await new Promise((r) => setTimeout(r, 100));
const skeleton = el.shadowRoot?.getElementById("loading-skeleton");
await waitUntil(
() =>
el.shadowRoot !== null &&
el.shadowRoot.getElementById("loading-skeleton") === null,
"Expected shadowRoot to be attached without a loading skeleton"
);
const skeleton = el.shadowRoot!.getElementById("loading-skeleton");

Copilot uses AI. Check for mistakes.
expect(skeleton).toBeNull();
});

when("the widget finishes loading", async () => {
await renderPromise;
});

then("no loading skeleton is present in the shadow DOM", () => {
const skeleton = el.shadowRoot?.getElementById("loading-skeleton");
expect(skeleton).toBeNull();
});
});

test("Loading skeleton type is determined by the widget type", ({
given,
and,
Expand Down
Loading