summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/browser/previews/browser_thumbnails.js
blob: 4fbbafccf9c3065e5c27ebd4401ac99df01df56c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* Any copyright is dedicated to the Public Domain.
 * https://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests PlacesPreviews.sys.mjs
 */
const { PlacesPreviews } = ChromeUtils.importESModule(
  "resource://gre/modules/PlacesPreviews.sys.mjs"
);
const { PlacesTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/PlacesTestUtils.sys.mjs"
);

const TEST_URL1 = "https://example.com/";
const TEST_URL2 = "https://example.org/";

/**
 * Counts tombstone entries.
 * @returns {integer} number of tombstone entries.
 */
async function countTombstones() {
  await PlacesTestUtils.promiseAsyncUpdates();
  let db = await PlacesUtils.promiseDBConnection();
  return (
    await db.execute("SELECT count(*) FROM moz_previews_tombstones")
  )[0].getResultByIndex(0);
}

add_task(async function test_thumbnail() {
  registerCleanupFunction(async () => {
    await PlacesUtils.history.clear();
    // Ensure tombstones table has been emptied.
    await TestUtils.waitForCondition(async () => {
      return (await countTombstones()) == 0;
    });
    PlacesPreviews.testSetDeletionTimeout(null);
  });
  // Sanity check initial state.
  Assert.equal(await countTombstones(), 0, "There's no tombstone entries");

  info("Test preview creation and storage.");
  await BrowserTestUtils.withNewTab(TEST_URL1, async browser => {
    await retryUpdatePreview(browser.currentURI.spec);
    let filePath = PlacesPreviews.getPathForUrl(TEST_URL1);
    Assert.ok(await IOUtils.exists(filePath), "The screenshot exists");
    Assert.equal(
      filePath.substring(filePath.lastIndexOf(".")),
      PlacesPreviews.fileExtension,
      "Check extension"
    );
    await testImageFile(filePath);
    await testMozPageThumb(TEST_URL1);
  });
});

add_task(async function test_page_removal() {
  info("Store another preview and test page removal.");
  await BrowserTestUtils.withNewTab(TEST_URL2, async browser => {
    await retryUpdatePreview(browser.currentURI.spec);
    let filePath = PlacesPreviews.getPathForUrl(TEST_URL2);
    Assert.ok(await IOUtils.exists(filePath), "The screenshot exists");
  });

  // Set deletion time to a small value so it runs immediately.
  PlacesPreviews.testSetDeletionTimeout(0);
  info("Wait for deletion, check one preview is removed, not the other one.");
  let promiseDeleted = new Promise(resolve => {
    PlacesPreviews.once("places-preview-deleted", (topic, filePath) => {
      resolve(filePath);
    });
  });
  await PlacesUtils.history.remove(TEST_URL1);

  let deletedFilePath = await promiseDeleted;
  Assert.ok(
    !(await IOUtils.exists(deletedFilePath)),
    "Check deleted file has been removed"
  );

  info("Check tombstones table has been emptied.");
  Assert.equal(await countTombstones(), 0, "There's no tombstone entries");

  info("Check the other thumbnail has not been removed.");
  let path = PlacesPreviews.getPathForUrl(TEST_URL2);
  Assert.ok(await IOUtils.exists(path), "Check non-deleted url is still there");
  await testImageFile(path);
  await testMozPageThumb(TEST_URL2);
});

add_task(async function async_test_deleteOrphans() {
  let path = PlacesPreviews.getPathForUrl(TEST_URL2);
  Assert.ok(await IOUtils.exists(path), "Sanity check one preview exists");
  // Create a file in the given path that doesn't have an entry in Places.
  let fakePath = PathUtils.join(
    PlacesPreviews.getPath(),
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + PlacesPreviews.fileExtension
  );
  // File contents don't matter.
  await IOUtils.writeJSON(fakePath, { test: true });
  let promiseDeleted = new Promise(resolve => {
    PlacesPreviews.once("places-preview-deleted", (topic, filePath) => {
      resolve(filePath);
    });
  });

  await PlacesPreviews.deleteOrphans();
  let deletedFilePath = await promiseDeleted;
  Assert.equal(deletedFilePath, fakePath, "Check orphan has been deleted");
  Assert.equal(await countTombstones(), 0, "There's no tombstone entries left");
  Assert.ok(
    !(await IOUtils.exists(fakePath)),
    "Ensure orphan has been deleted"
  );

  Assert.ok(await IOUtils.exists(path), "Ensure valid preview is still there");
});

async function testImageFile(path) {
  info("Load the file and check its content type.");
  const buffer = await IOUtils.read(path);
  const fourcc = new TextDecoder("utf-8").decode(buffer.slice(8, 12));
  Assert.equal(fourcc, "WEBP", "Check the stored preview is webp");
}

async function testMozPageThumb(url) {
  info("Check moz-page-thumb protocol: " + PlacesPreviews.getPageThumbURL(url));
  let { data, contentType } = await fetchImage(
    PlacesPreviews.getPageThumbURL(url)
  );
  Assert.equal(
    contentType,
    PlacesPreviews.fileContentType,
    "Check the content type"
  );
  const fourcc = data.slice(8, 12);
  Assert.equal(fourcc, "WEBP", "Check the returned preview is webp");
}

function fetchImage(url) {
  return new Promise((resolve, reject) => {
    NetUtil.asyncFetch(
      {
        uri: NetUtil.newURI(url),
        loadUsingSystemPrincipal: true,
        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE,
      },
      (input, status, request) => {
        if (!Components.isSuccessCode(status)) {
          reject(new Error("unable to load image"));
          return;
        }

        try {
          let data = NetUtil.readInputStreamToString(input, input.available());
          let contentType = request.QueryInterface(Ci.nsIChannel).contentType;
          input.close();
          resolve({ data, contentType });
        } catch (ex) {
          reject(ex);
        }
      }
    );
  });
}

/**
 * Sometimes on macOS fetching the preview fails for timeout/network reasons,
 * this retries so the test doesn't intermittently fail over it.
 * @param {string} url The url to store a preview for.
 * @returns {Promise} resolved once a preview has been captured.
 */
function retryUpdatePreview(url) {
  return TestUtils.waitForCondition(() => PlacesPreviews.update(url));
}