Skip to content
160 changes: 159 additions & 1 deletion core/src/thing/Thing.anyValue.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { graph, IndexedFormula, sym } from "rdflib";
import { graph, IndexedFormula, literal, quad, sym } from "rdflib";
import { PodOsSession } from "../authentication";
import { Thing } from "./Thing";
import { Store } from "../Store";
import { Subscription } from "rxjs";

describe("Thing", function () {
describe("any value", () => {
Expand Down Expand Up @@ -91,4 +92,161 @@ describe("Thing", function () {
expect(result).toBe("test value");
});
});

describe("observeAnyValue", () => {
jest.useFakeTimers();

let internalStore: IndexedFormula,
subscriber: jest.Mock,
uri: string,
predicate: string,
anyValueSpy: jest.SpyInstance,
subscription: Subscription;

beforeEach(() => {
// Given a store with statements about a URI
internalStore = graph();
uri = "https://jane.doe.example/container/file.ttl#fragment";
predicate = "https://vocab.test/predicate";
internalStore.add(sym(uri), sym(predicate), "literal value");
internalStore.add(
sym(uri),
sym("https://vocab.test/other-predicate"),
"literal value",
);

const mockSession = {} as unknown as PodOsSession;
const store = new Store(mockSession, undefined, undefined, internalStore);

// and a Thing with a anyValue method
subscriber = jest.fn();
const thing = new Thing(uri, store);
anyValueSpy = jest.spyOn(thing, "anyValue");

// and a subscription to changes in value
const observable = thing.observeAnyValue(predicate);
subscription = observable.subscribe(subscriber);
});

it("pushes existing value immediately", () => {
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber.mock.calls).toEqual([["literal value"]]);
});

it("pushes undefined if single existing value is removed", () => {
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(2);
expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber.mock.lastCall).toEqual([undefined]);
});

it("stops pushing after unsubscribe", () => {
subscription.unsubscribe();
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
jest.advanceTimersByTime(250);
expect(subscriber).toHaveBeenCalledTimes(1);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
});

it("ignores irrelevant removal", () => {
internalStore.removeStatement(
quad(
sym(uri),
sym("https://vocab.test/other-predicate"),
literal("literal value"),
),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledTimes(1);
});

it("does not change if a value is already present", () => {
internalStore.add(sym(uri), sym(predicate), "literal value 2");
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledTimes(1);
});

it("pushes in groups", () => {
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
internalStore.add(sym(uri), sym(predicate), "new literal value");
jest.advanceTimersByTime(250);
expect(subscriber).toHaveBeenCalledTimes(2);
});

it("pushes a value if none was present, without calling anyValue again", () => {
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
internalStore.add(sym(uri), sym(predicate), "new literal value");
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber.mock.lastCall).toEqual(["new literal value"]);
});

it("pushes a different value when one is removed if multiple are present", () => {
internalStore.add(sym(uri), sym(predicate), "literal value 2");
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(2);
expect(subscriber.mock.lastCall).toEqual(["literal value 2"]);
});

it("does not change again until next removal", () => {
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
internalStore.add(
quad(sym(uri), sym(predicate), literal("literal value 2")),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber.mock.lastCall).toEqual(["literal value 2"]);

internalStore.add(
quad(sym(uri), sym(predicate), literal("literal value 3")),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledTimes(2);

internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value 2")),
);
jest.advanceTimersByTime(250);
expect(anyValueSpy).toHaveBeenCalledTimes(2);
expect(subscriber).toHaveBeenCalledTimes(3);
expect(subscriber.mock.lastCall).toEqual(["literal value 3"]);
});

it("ignores irrelevant addition", () => {
internalStore.removeStatement(
quad(sym(uri), sym(predicate), literal("literal value")),
);
internalStore.add(
sym(uri),
sym("https://vocab.test/other-predicate"),
literal("wrong literal value"),
);
internalStore.add(
sym(uri),
sym(predicate),
literal("right literal value"),
);
jest.advanceTimersByTime(250);
expect(subscriber.mock.lastCall).toEqual(["right literal value"]);
});
});
});
152 changes: 151 additions & 1 deletion core/src/thing/Thing.label.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { graph, IndexedFormula, sym } from "rdflib";
import { graph, IndexedFormula, literal, quad, sym } from "rdflib";
import { PodOsSession } from "../authentication";
import { Thing } from "./Thing";
import { Store } from "../Store";
Expand Down Expand Up @@ -81,4 +81,154 @@ describe("Thing", function () {
expect(result).toEqual("literal value");
});
});

describe("observeLabel", () => {
const uri = "https://jane.doe.example/container/file.ttl#fragment";

it("pushes existing value immediately", () => {
// Given a store with statements about a URI
const internalStore = graph();
internalStore.add(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
"literal value",
);

const mockSession = {} as unknown as PodOsSession;
const store = new Store(mockSession, undefined, undefined, internalStore);

// and a Thing
const subscriber = jest.fn();
const thing = new Thing(uri, store);

// and a subscription to changes of label
const observable = thing.observeLabel();
observable.subscribe(subscriber);

expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber.mock.calls).toEqual([["literal value"]]);
});

it("uses same value as label() when nothing is present in store", () => {
// Given an empty store
const mockSession = {} as unknown as PodOsSession;
const store = new Store(mockSession);

// and a Thing
const subscriber = jest.fn();
const thing = new Thing(uri, store);

// and a subscription to changes of label
const observable = thing.observeLabel();
observable.subscribe(subscriber);

expect(subscriber.mock.lastCall).toEqual([thing.label()]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

While observeLabel doesn't call label at all, it does call labelFromUri so I thought this might be a sufficient test without duplicating all of "if nothing is found in the store".

From a TDD perspective, I wrote this as a failing test before implementing

.pipe(map((value) => value ?? labelFromUri(this.uri)));

});

it.each([
"http://xmlns.com/foaf/0.1/nick",
"http://purl.org/dc/terms/title",
"http://purl.org/dc/elements/1.1/title",
"http://xmlns.com/foaf/0.1/name",
"http://schema.org/name",
"https://schema.org/name",
"http://www.w3.org/2000/01/rdf-schema#label",
"https://www.w3.org/ns/activitystreams#name",
"http://www.w3.org/2006/vcard/ns#fn",
"http://schema.org/caption",
"https://schema.org/caption",
])("returns the literal value of predicate %s", (predicate: string) => {
// Given a store with label for a URI using this predicate
const internalStore = graph();
internalStore.add(sym(uri), sym(predicate), "literal value");
const mockSession = {} as unknown as PodOsSession;
const store = new Store(mockSession, undefined, undefined, internalStore);

// and a Thing
const subscriber = jest.fn();
const thing = new Thing(uri, store);

// and a subscription to changes of label
const observable = thing.observeLabel();
observable.subscribe(subscriber);

expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber.mock.calls).toEqual([["literal value"]]);
});

describe("follows observeAnyValue behaviour", () => {
jest.useFakeTimers();

let internalStore: IndexedFormula, subscriber: jest.Mock;

beforeEach(() => {
// Given a store with a URI with a label
internalStore = graph();
internalStore.add(
quad(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
literal("literal value"),
),
);

const mockSession = {} as unknown as PodOsSession;
const store = new Store(
mockSession,
undefined,
undefined,
internalStore,
);

// and a Thing
subscriber = jest.fn();
const thing = new Thing(uri, store);

// and a subscription to changes of label
const observable = thing.observeLabel();
observable.subscribe(subscriber);
});

it("does not push if a label already exists", () => {
internalStore.add(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
"literal value 2",
);
jest.advanceTimersByTime(250);
expect(subscriber).toHaveBeenCalledTimes(1);
});

it("pushes on remove", () => {
internalStore.removeStatement(
quad(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
literal("literal value"),
),
);
jest.advanceTimersByTime(250);
expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber.mock.lastCall).toEqual(["fragment"]);
});

it("pushes in a group when label changes", () => {
internalStore.removeStatement(
quad(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
literal("literal value"),
),
);
internalStore.add(
sym(uri),
sym("http://www.w3.org/2000/01/rdf-schema#label"),
"literal value 2",
);
jest.advanceTimersByTime(250);
expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber.mock.lastCall).toEqual(["literal value 2"]);
});
});
});
});
Loading