Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
09b8730
Added keepAlive option to src object to set the renderer preventClean…
michielvandergeest Jun 29, 2026
b63efcc
Added documentation about the object format for src.
michielvandergeest Jun 29, 2026
44fe8f1
Asbtracted announcer (and speechSynthesis) to platform layer to make …
michielvandergeest Jun 29, 2026
b86a62a
Resolved node undefined issues while accessing texture
suresh-gangumalla Jun 29, 2026
52297e3
Fixed typo in announcer when interrupted.
michielvandergeest Jun 30, 2026
3f0136d
Merge pull request #693 from lightning-js/feature/platform-abstracted…
michielvandergeest Jun 30, 2026
dbe6ee0
Merge pull request #694 from lightning-js/fix/src-keepAlive-issue
michielvandergeest Jun 30, 2026
613580f
Added a webOS platform specific announcer implementation based on luna.
michielvandergeest Jun 30, 2026
b4c8c67
Added findByData and findAllByData methods to test-harness.
michielvandergeest Jul 1, 2026
e254daf
Updated findByData method to look for only single item
suresh-gangumalla Jul 1, 2026
515a37a
Added test verifying frameTick and idle renderer listeners are unregi…
il-sairamg Jul 1, 2026
2f05ebe
Merge pull request #697 from lightning-js/update/find-element-by-dat
michielvandergeest Jul 1, 2026
2677dd1
Merge pull request #698 from il-sairamg/test/verify-renderer-hook-cle…
michielvandergeest Jul 2, 2026
44150d5
Merge pull request #696 from lightning-js/feature/test-harness-find-e…
michielvandergeest Jul 2, 2026
933f849
Merge pull request #692 from lightning-js/feature/src-keepAlive-option
michielvandergeest Jul 2, 2026
54d702d
Merge pull request #695 from lightning-js/feature/webos-luna-announcer
michielvandergeest Jul 2, 2026
8980190
Bumped version to 2.7.0 and updated changelog.
michielvandergeest Jul 2, 2026
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: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.7.0

_2 jul 2026_

- Added `keepAlive` option to `src`-object to prevent a texture from being cleaned up by the renderer
- Abstracted announcer (and speechSynthesis) to be platform provided (with speechSynthesis as the built-in default)
- Added dedicated findByData methods to testing harness to select specific nodes in the tree
- Improved test cases around frameTick cleanup

## 2.6.0

_25 jun 2026_
Expand Down
12 changes: 12 additions & 0 deletions docs/essentials/displaying_images.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ It is recommended to give your Element a width (`w`) and a height (`h`) attribut

For the best performance, it's important to keep your source images as small as possible. If you're displaying an image at `200px x 200px`, make sure the image is exactly that size or _smaller_. The latter option may lead to some quality loss, but can positively impact the overall performance of your App.

## Advanced src Attribute

Besides passing a string to the `src` attribute, you can also pass an object with extra options.

```xml
<Element :src="{src: 'assets/background', type: 'ktx', keepAlive: true}" w="1920" h="1080" />
```

The `type` option can be used to explicitly set the image type. This is useful when the type can not be derived from the url or filename, or when you want to use a compressed texture format like `ktx`.

The `keepAlive` option prevents the texture generated from the source image from being cleaned up. Use this for images that are reused often and should stay available in memory.

## Colorization

You also have the option to _colorize_ an image on the fly. Just add a `color` attribute to the Element with a `src` attribute. You can use a single color, or apply a gradient.
Expand Down
33 changes: 32 additions & 1 deletion docs/essentials/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,38 @@ Blits.Launch(App, 'app', {
})
```

Common platform properties that can be overwritten are `input`, `viewport`, `dispatchEvent`, `localStorage`, `getCookie`, `setCookie`, `historyBack`, `screenHeight`, `hardwareConcurrency`, `userAgent`, `KeyboardEvent`, `isKeyboardEvent`, `createKeyboardEvent`, `SpeechSynthesisUtterance`, `speechSynthesis`, and `now`.
Common platform properties that can be overwritten are `input`, `viewport`, `dispatchEvent`, `localStorage`, `getCookie`, `setCookie`, `historyBack`, `screenHeight`, `hardwareConcurrency`, `userAgent`, `KeyboardEvent`, `isKeyboardEvent`, `createKeyboardEvent`, `announcer`, and `now`.

The `announcer` platform property can be used to provide a custom text-to-speech driver. When set, Blits keeps using the built-in announcer queue and calls the custom driver's `speak(options)` and `cancel()` methods instead of the default Web Speech implementation.

```js
Blits.Launch(App, 'app', {
announcer: true,
platform: (defaults) => ({
announcer: {
speak(options) {
return myPlatformSpeech.speak(options.message)
},
cancel() {
myPlatformSpeech.cancel()
},
},
}),
})
```

For LG webOS apps, Blits provides a webOS announcer factory that calls the platform TTS service.

```js
import createAnnouncer from '@lightningjs/blits/platforms/webOS/announcer'

Blits.Launch(App, 'app', {
announcer: true,
platform: () => ({
announcer: createAnnouncer(),
}),
})
```

The `rendererPlatform` setting is separate from `platform`. It is only passed to the renderer, and should be used for renderer specific platform configuration.

Expand Down
17 changes: 17 additions & 0 deletions docs/testing/test-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ The snapshot contains:

Nested Components are included in the `tree` as Component snapshots as well.

## Finding Nodes by Inspector Data

Use `findByData()` and `findAllByData()` to find nodes with matching `inspector-data`.

```xml
<Text content="Menu" inspector-data="{testId: 'menu-title'}" />
<Element inspector-data="{role: 'menu-item'}" />
```

```js
const title = fixture.findByData('testId', 'menu-title')
const items = fixture.findAllByData('role', 'menu-item')
```

`findByData(key, value)` returns the first matching node or `null`.
`findAllByData(key, value)` returns all matching nodes, or an empty array when nothing matches.

## Updating Props and State

Props can be updated with `setProps()`.
Expand Down
30 changes: 24 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ declare module '@lightningjs/blits' {
enableUtteranceKeepAlive?: boolean
}

export interface AnnouncerDriverOptions extends AnnouncerUtteranceOptions {
/**
* Message to be spoken by the platform announcer driver.
*/
message: string | number,
/**
* Internal announcement id used by the Blits announcer queue.
*/
id: number,
}

export interface AnnouncerDriver {
/**
* Speak one queued announcement.
*/
speak(options: AnnouncerDriverOptions): Promise<any>,
/**
* Stop the active platform announcement, when supported.
*/
cancel(): void,
}

export interface AnnouncerUtterance<T = any> extends Promise<T> {
/**
* Removes a specific message from the announcement queue,
Expand Down Expand Up @@ -1070,13 +1092,9 @@ declare module '@lightningjs/blits' {
*/
createKeyboardEvent?: (type: string, init?: KeyboardEventInit | any) => any,
/**
* SpeechSynthesisUtterance constructor used by the announcer.
*/
SpeechSynthesisUtterance?: typeof SpeechSynthesisUtterance | any,
/**
* Speech synthesis API implementation used by the announcer.
* Platform-specific announcer driver used instead of the default Web Speech driver.
*/
speechSynthesis?: SpeechSynthesis | any,
announcer?: AnnouncerDriver,
/**
* Monotonic timestamp provider used for input throttling.
*/
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.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lightningjs/blits",
"version": "2.6.0",
"version": "2.7.0",
"description": "Blits: The Lightning 3 App Development Framework",
"bin": "bin/index.js",
"exports": {
Expand Down Expand Up @@ -32,6 +32,10 @@
"types": "./src/plugins/storage/storage.d.ts",
"default": "./src/plugins/storage/storage.js"
},
"./platforms/webOS/announcer": {
"types": "./src/platforms/webOS/announcer.d.ts",
"default": "./src/platforms/webOS/announcer.js"
},
"./testing": {
"types": "./src/testing/index.d.ts",
"default": "./src/testing/index.js"
Expand Down
35 changes: 29 additions & 6 deletions src/announcer/announcer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/

import { Log } from '../lib/log.js'
import speechSynthesis from './speechSynthesis.js'
import { platform } from '../platform.js'

let active = false
Expand All @@ -31,6 +30,20 @@ let globalDefaultOptions = {
enableUtteranceKeepAlive: !/android/i.test(platform.userAgent || ''),
}

const getDriver = () => {
const driver = platform.announcer
if (driver && typeof driver.speak === 'function') {
return driver
}
}

const cancelDriver = () => {
const driver = getDriver()
if (driver && typeof driver.cancel === 'function') {
driver.cancel()
}
}

const noopAnnouncement = {
then() {},
done() {},
Expand Down Expand Up @@ -84,9 +97,9 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
// augment the promise with a stop function
done.stop = () => {
if (id === currentId) {
speechSynthesis.cancel()
cancelDriver()
isProcessing = false
resolveFn('interupted')
resolveFn('interrupted')
}
}

Expand Down Expand Up @@ -133,7 +146,17 @@ const processQueue = async () => {
debounce = setTimeout(() => {
Log.debug(`Announcer - speaking: "${message}" (id: ${id})`)

speechSynthesis
const driver = getDriver()
if (driver === undefined) {
currentId = null
currentResolveFn = null
isProcessing = false
resolveFn('unavailable')
processQueue()
return
}

driver
.speak({
message,
id,
Expand Down Expand Up @@ -180,8 +203,8 @@ const stop = () => {
// Clear debounce timer if speech hasn't started yet
clearDebounceTimer()

// Always cancel speech synthesis to ensure clean state
speechSynthesis.cancel()
// Always cancel the platform announcer to ensure clean state
cancelDriver()

// Store resolve function before resetting state
const resolveFn = currentResolveFn
Expand Down
69 changes: 65 additions & 4 deletions src/announcer/announcer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import test from 'tape'

// Mock speechSynthesis API before importing announcer (which imports speechSynthesis)
// Mock Web Speech API for the default announcer driver
const mockUtterance = class {
constructor(text) {
this.text = text
Expand Down Expand Up @@ -62,16 +62,15 @@ const mockSpeechSynthesis = {
},
}

// Setup mocks before module loads (speechSynthesis.js captures window.speechSynthesis at import time)
window.speechSynthesis = mockSpeechSynthesis
window.SpeechSynthesisUtterance = mockUtterance
globalThis.SpeechSynthesisUtterance = mockUtterance

// Dynamic import to ensure mocks are set before module evaluation
const announcerModule = await import('./announcer.js')
const announcer = announcerModule.default

import { initLog } from '../lib/log.js'
import { configurePlatform } from '../platform.js'

initLog()
announcer.enable()
Expand Down Expand Up @@ -229,7 +228,7 @@ test('Announcer stop interrupts processing', (assert) => {
// Due to timing, it may finish before stop is called
assert.ok(
status === 'interrupted' || status === 'unavailable' || status === 'finished',
'Stop interrupts (or unavailable if no speechSynthesis, or finished if completed)'
'Stop interrupts (or unavailable if Web Speech is missing, or finished if completed)'
)
assert.end()
})
Expand Down Expand Up @@ -277,3 +276,65 @@ test('Announcer queue behavior', (assert) => {
assert.end()
})
})

test('Announcer uses custom platform announcer driver', (assert) => {
announcer.stop()
announcer.clear()
announcer.enable()

const calls = []

configurePlatform((defaults) => ({
...defaults,
announcer: {
speak(options) {
calls.push(options)
return Promise.resolve()
},
cancel() {},
},
}))

announcer.speak('custom driver message', 'off', { lang: 'nl-NL' }).then((status) => {
assert.equal(status, 'finished', 'custom driver announcement resolves')
assert.equal(calls.length, 1, 'custom driver speak is called once')
assert.equal(calls[0].message, 'custom driver message', 'custom driver receives message')
assert.equal(calls[0].lang, 'nl-NL', 'custom driver receives options')

configurePlatform(() => ({}))
assert.end()
})
})

test('Announcer stop uses custom platform announcer driver cancel', (assert) => {
announcer.stop()
announcer.clear()
announcer.enable()

let cancelCount = 0

configurePlatform((defaults) => ({
...defaults,
announcer: {
speak() {
return new Promise(() => {})
},
cancel() {
cancelCount++
},
},
}))

const announcement = announcer.speak('custom driver stop')
announcement.then((status) => {
assert.equal(status, 'interrupted', 'announcement resolves as interrupted')
assert.equal(cancelCount, 1, 'custom driver cancel is called')

configurePlatform(() => ({}))
assert.end()
})

setTimeout(() => {
announcement.stop()
}, 400)
})
Loading
Loading