@structured-id/ui-core 4.7.0 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@structured-id/ui-core",
3
- "version": "4.7.0",
3
+ "version": "4.7.2",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -6,6 +6,16 @@ import { GrantOutcome } from "@structured-id/proto/sid/v1/authn/application_sess
6
6
  const host = vi.hoisted(() => ({
7
7
  getApplicationSession: vi.fn(),
8
8
  endApplicationSession: vi.fn(),
9
+ /** The base URL of the transport last made. */
10
+ transportBase: undefined as string | undefined,
11
+ }));
12
+
13
+ vi.mock("@protobuf-ts/grpcweb-transport", () => ({
14
+ GrpcWebFetchTransport: class {
15
+ constructor(options: { baseUrl: string }) {
16
+ host.transportBase = options.baseUrl;
17
+ }
18
+ },
9
19
  }));
10
20
 
11
21
  vi.mock("@structured-id/proto/sid/v1/authn/application_session.client", () => ({
@@ -40,6 +50,27 @@ const LIVE = {
40
50
  csrfToken: "token-1",
41
51
  };
42
52
 
53
+ describe("the session service's address", () => {
54
+ // The host serves a page's calls only under its API mount: a call at the
55
+ // root is refused, and the page cannot read a session it has.
56
+ it("is the API mount of the page's own origin", () => {
57
+ initBff("");
58
+ expect(host.transportBase).toBe("/api");
59
+ });
60
+
61
+ it("is the API mount of a configured host", () => {
62
+ initBff("https://host.sid.example.com");
63
+ expect(host.transportBase).toBe("https://host.sid.example.com/api");
64
+ });
65
+
66
+ it("is the API mount of a host named for one call", async () => {
67
+ initBff("");
68
+ host.getApplicationSession.mockReturnValue(answers(LIVE));
69
+ await checkBffSession("https://other.sid.example.com");
70
+ expect(host.transportBase).toBe("https://other.sid.example.com/api");
71
+ });
72
+ });
73
+
43
74
  describe("checkBffSession", () => {
44
75
  beforeEach(() => {
45
76
  vi.clearAllMocks();
@@ -43,10 +43,13 @@ let _session: ApplicationSessionServiceClient | null = null;
43
43
  */
44
44
  let _csrfToken: string | null = null;
45
45
 
46
- /** The session service at `baseUrl`; the cookie never travels to another origin. */
46
+ /**
47
+ * The session service of the host at `baseUrl`, under the `/api` mount that
48
+ * serves a page's calls; the cookie never travels to another origin.
49
+ */
47
50
  function sessionClient(baseUrl: string): ApplicationSessionServiceClient {
48
51
  const transport: RpcTransport = new GrpcWebFetchTransport({
49
- baseUrl,
52
+ baseUrl: `${baseUrl}/api`,
50
53
  fetchInit: { credentials: "same-origin" },
51
54
  });
52
55
  return new ApplicationSessionServiceClient(transport);
@@ -234,6 +234,29 @@ describe("SidPasswordCeremony", () => {
234
234
  expect(w.emitted("settled")).toEqual([["refused"]]);
235
235
  });
236
236
 
237
+ // With reduced motion the line still shows how far the operation went and
238
+ // that it is running: settled positions are solid, open ones are marked
239
+ // (they dissolve slowly instead of churning), so a running operation is
240
+ // never a frozen row of dots.
241
+ it("shows progress and activity under reduced motion", async () => {
242
+ vi.stubGlobal(
243
+ "matchMedia",
244
+ (query: string) =>
245
+ ({ matches: query.includes("reduce") }) as MediaQueryList,
246
+ );
247
+ const w = mountCeremony({ length: 10 });
248
+ await merged();
249
+ const settled = () =>
250
+ rows(w)[0].findAll(".sid-ceremony__glyph--set").length;
251
+ const open = () => rows(w)[0].findAll(".sid-ceremony__glyph--open").length;
252
+ expect(settled()).toBe(0);
253
+ expect(open()).toBe(10);
254
+ await w.setProps({ progress: at("prove", 0.5, "Building the proof") });
255
+ await vi.advanceTimersByTimeAsync(80);
256
+ expect(settled()).toBe(5);
257
+ expect(open()).toBe(5);
258
+ });
259
+
237
260
  // With reduced motion nothing churns and every step lands at once.
238
261
  it("does not animate under reduced motion", async () => {
239
262
  // jsdom has no matchMedia: the browser's answer is given here.
@@ -41,7 +41,11 @@
41
41
  v-for="(glyph, i) in glyphs"
42
42
  :key="i"
43
43
  class="sid-ceremony__glyph"
44
- :class="{ 'sid-ceremony__glyph--set': glyph === DOT }"
44
+ :class="
45
+ i < settledUpTo
46
+ ? 'sid-ceremony__glyph--set'
47
+ : 'sid-ceremony__glyph--open'
48
+ "
45
49
  >{{ glyph }}</span
46
50
  >
47
51
  </div>
@@ -150,6 +154,12 @@ const stageHeight = computed(
150
154
  );
151
155
  const count = computed(() => Math.max(1, Math.min(props.length, MAX_GLYPHS)));
152
156
  const glyphs = ref<string[]>(Array.from({ length: count.value }, () => DOT));
157
+ /**
158
+ * How many positions, from the left, the operation has settled. Kept apart
159
+ * from what the glyphs show: under reduced motion every position is a dot,
160
+ * and only this tells a settled one from an open one.
161
+ */
162
+ const settledUpTo = ref(0);
153
163
 
154
164
  const phase = ref<Phase>("split");
155
165
  const checked = ref(false);
@@ -249,12 +259,12 @@ const label = computed(() => {
249
259
  let ticks = 0;
250
260
  function tick() {
251
261
  pace();
252
- const settledUpTo = Math.floor(fraction.value * count.value);
262
+ settledUpTo.value = Math.floor(fraction.value * count.value);
253
263
  // The open digits change every fourth frame (about six times a second);
254
264
  // the line itself follows the pace every frame.
255
265
  const churning = !still && ticks++ % 4 === 0;
256
266
  glyphs.value = glyphs.value.map((glyph, i) => {
257
- if (i < settledUpTo) return DOT;
267
+ if (i < settledUpTo.value) return DOT;
258
268
  if (!churning) return still ? DOT : glyph;
259
269
  // A calm churn: about a third of the open digits change at a time.
260
270
  if (glyph !== DOT && Math.random() > 0.35) return glyph;
@@ -277,6 +287,7 @@ function stopChurn() {
277
287
  function close() {
278
288
  stopChurn();
279
289
  fraction.value = 1;
290
+ settledUpTo.value = count.value;
280
291
  glyphs.value = glyphs.value.map(() => DOT);
281
292
  phase.value = "orb";
282
293
  after(TIMING.orb, () => {
@@ -378,6 +389,20 @@ onBeforeUnmount(() => {
378
389
  opacity: 1;
379
390
  transform: scale(0.6);
380
391
  }
392
+ /* Under reduced motion the open positions dissolve slowly in place instead
393
+ of churning: no movement, but a running operation never looks frozen. */
394
+ .sid-ceremony--still .sid-ceremony__glyph--open {
395
+ transform: scale(0.6);
396
+ animation: sid-ceremony-dissolve 1.6s ease-in-out infinite alternate;
397
+ }
398
+ @keyframes sid-ceremony-dissolve {
399
+ from {
400
+ opacity: 0.15;
401
+ }
402
+ to {
403
+ opacity: 0.55;
404
+ }
405
+ }
381
406
  .sid-ceremony__orb {
382
407
  position: absolute;
383
408
  top: 50%;