vue_uk 1.0.0
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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +21 -0
- data/.github/workflows/release.yml +19 -0
- data/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +10 -0
- data/COMM-LICENSE +22 -0
- data/Gemfile +3 -0
- data/LICENSE +617 -0
- data/README.md +141 -0
- data/Rakefile +35 -0
- data/lib/vue_uk/cinema.rb +115 -0
- data/lib/vue_uk/internal/api_response.rb +51 -0
- data/lib/vue_uk/internal/http.rb +42 -0
- data/lib/vue_uk/internal/parser/api/showtime.rb +73 -0
- data/lib/vue_uk/internal/title_sanitizer.rb +31 -0
- data/lib/vue_uk/internal/website.rb +22 -0
- data/lib/vue_uk/performance.rb +70 -0
- data/lib/vue_uk/version.rb +6 -0
- data/lib/vue_uk.rb +19 -0
- data/test/fixtures/api/cinemas.json +42 -0
- data/test/fixtures/api/films-10051.json +103 -0
- data/test/fixtures/website/cinema-10051.html +8 -0
- data/test/lib/vue_uk/cinema_test.rb +110 -0
- data/test/lib/vue_uk/internal/api_response_test.rb +68 -0
- data/test/lib/vue_uk/internal/fixture_shape_test.rb +18 -0
- data/test/lib/vue_uk/internal/parser/api/showtime_test.rb +44 -0
- data/test/lib/vue_uk/internal/title_sanitizer_test.rb +30 -0
- data/test/lib/vue_uk/internal/website_test.rb +22 -0
- data/test/lib/vue_uk/performance_test.rb +108 -0
- data/test/lib/vue_uk/version_test.rb +7 -0
- data/test/live/api_shape_test.rb +17 -0
- data/test/live/integration_test.rb +25 -0
- data/test/live/live_helper.rb +9 -0
- data/test/support/api_shapes.rb +38 -0
- data/test/support/fakes.rb +31 -0
- data/test/support/fixtures_helper.rb +21 -0
- data/test/test_helper.rb +20 -0
- data/vue_uk.gemspec +30 -0
- metadata +195 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"result": [
|
|
3
|
+
{
|
|
4
|
+
"filmId": "HO1",
|
|
5
|
+
"filmTitle": "The Example Film",
|
|
6
|
+
"showingGroups": [
|
|
7
|
+
{
|
|
8
|
+
"date": "2026-10-24T00:00:00",
|
|
9
|
+
"sessions": [
|
|
10
|
+
{
|
|
11
|
+
"sessionId": "1001",
|
|
12
|
+
"bookingUrl": "/book-tickets/summary/10051/HO1/1001",
|
|
13
|
+
"showTimeWithTimeZone": "2026-10-24T10:30:00+01:00",
|
|
14
|
+
"attributes": [
|
|
15
|
+
{ "name": "English", "shortName": "", "value": "english" },
|
|
16
|
+
{ "name": "Audio Description (AD)", "shortName": "AD", "value": "audio" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"sessionId": "1002",
|
|
21
|
+
"bookingUrl": "/book-tickets/summary/10051/HO1/1002",
|
|
22
|
+
"showTimeWithTimeZone": "2026-10-24T14:00:00+01:00",
|
|
23
|
+
"attributes": [
|
|
24
|
+
{ "name": "3D", "shortName": "3D", "value": "3d" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"date": "2026-10-25T00:00:00",
|
|
31
|
+
"sessions": [
|
|
32
|
+
{
|
|
33
|
+
"sessionId": "1009",
|
|
34
|
+
"bookingUrl": "/book-tickets/summary/10051/HO1/1009",
|
|
35
|
+
"showTimeWithTimeZone": "2026-10-25T10:30:00+00:00",
|
|
36
|
+
"attributes": []
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"filmId": "HO2",
|
|
44
|
+
"filmTitle": "Imaginary Picture (2026) (Tamil)",
|
|
45
|
+
"showingGroups": [
|
|
46
|
+
{
|
|
47
|
+
"date": "2026-10-24T00:00:00",
|
|
48
|
+
"sessions": [
|
|
49
|
+
{
|
|
50
|
+
"sessionId": "1003",
|
|
51
|
+
"bookingUrl": "/book-tickets/summary/10051/HO2/1003",
|
|
52
|
+
"showTimeWithTimeZone": "2026-10-24T18:00:00+01:00",
|
|
53
|
+
"attributes": [
|
|
54
|
+
{ "name": "Tamil", "shortName": "Tamil", "value": "tamil" },
|
|
55
|
+
{ "name": "Open Captioned", "shortName": "Open Captioned", "value": "open-captioned" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"filmId": "HO3",
|
|
64
|
+
"filmTitle": "Mystery Screening (24 October 2026)",
|
|
65
|
+
"showingGroups": [
|
|
66
|
+
{
|
|
67
|
+
"date": "2026-10-24T00:00:00",
|
|
68
|
+
"sessions": [
|
|
69
|
+
{
|
|
70
|
+
"sessionId": "1004",
|
|
71
|
+
"bookingUrl": "/book-tickets/summary/10051/HO3/1004",
|
|
72
|
+
"showTimeWithTimeZone": "2026-10-24T11:00:00+01:00",
|
|
73
|
+
"attributes": [
|
|
74
|
+
{ "name": "Mighty Mornings", "shortName": "Mighty Mornings", "value": "mightymornings" }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"filmId": "HO4",
|
|
83
|
+
"filmTitle": "CFF: Made Up Matinee",
|
|
84
|
+
"showingGroups": [
|
|
85
|
+
{
|
|
86
|
+
"date": "2026-10-24T00:00:00",
|
|
87
|
+
"sessions": [
|
|
88
|
+
{
|
|
89
|
+
"sessionId": "1005",
|
|
90
|
+
"bookingUrl": "/book-tickets/summary/10051/HO4/1005",
|
|
91
|
+
"showTimeWithTimeZone": "2026-10-25T00:15:00+01:00",
|
|
92
|
+
"attributes": [
|
|
93
|
+
{ "name": "Big Screen Events", "shortName": "", "value": "big-screen-events" }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"responseCode": 0,
|
|
102
|
+
"errorMessage": null
|
|
103
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head><title>Vue Accrington</title></head>
|
|
4
|
+
<body>
|
|
5
|
+
<div id="__next"></div>
|
|
6
|
+
<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"layoutData":{"sitecore":{"context":{"cinema":{"cinemaId":{"value":"10051"},"cinemaName":{"value":"Accrington"},"cinemaAddress":{"value":"1 Example Viaduct\r\nExample Road\r\nAccrington\r\nBB5 0ZZ "}}}}}}}}</script>
|
|
7
|
+
</body>
|
|
8
|
+
</html>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require_relative '../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::CinemaTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@api_response = FakeApiResponse.new
|
|
6
|
+
VueUk::Cinema.instance_variable_set(:@cinemas_hash, nil)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_new_stores_string_id
|
|
10
|
+
assert_equal '10051', VueUk::Cinema.new(10051).id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_all_returns_uk_cinemas
|
|
14
|
+
with_fakes do
|
|
15
|
+
cinemas = VueUk::Cinema.all
|
|
16
|
+
|
|
17
|
+
assert(cinemas.all? { |cinema| cinema.instance_of?(VueUk::Cinema) })
|
|
18
|
+
assert_equal %w(10051 10015), cinemas.map(&:id)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_name_and_full_name
|
|
23
|
+
with_fakes do
|
|
24
|
+
assert_equal 'Accrington', VueUk::Cinema.new(10051).name
|
|
25
|
+
assert_equal 'Vue Birmingham (Star City)', VueUk::Cinema.new(10015).full_name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_brand
|
|
30
|
+
assert_equal 'Vue', VueUk::Cinema.new(10051).brand
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_slug
|
|
34
|
+
with_fakes do
|
|
35
|
+
assert_equal 'vue-birmingham-star-city', VueUk::Cinema.new(10015).slug
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_url
|
|
40
|
+
with_fakes do
|
|
41
|
+
assert_equal 'https://www.myvue.com/cinema/accrington', VueUk::Cinema.new(10051).url
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_adr_from_cinema_page
|
|
46
|
+
expected = {
|
|
47
|
+
street_address: '1 Example Viaduct',
|
|
48
|
+
extended_address: 'Example Road',
|
|
49
|
+
locality: 'Accrington',
|
|
50
|
+
region: nil,
|
|
51
|
+
postal_code: 'BB5 0ZZ',
|
|
52
|
+
country_name: 'United Kingdom'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
with_fakes do
|
|
56
|
+
assert_equal expected, VueUk::Cinema.new(10051).adr
|
|
57
|
+
assert_equal 'BB5 0ZZ', VueUk::Cinema.new(10051).postal_code
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_adr_with_country_after_postcode
|
|
62
|
+
website = Object.new
|
|
63
|
+
website.define_singleton_method(:cinema) do |_url|
|
|
64
|
+
{ 'cinemaAddress' => { 'value' => "2 Example Park\r\nExample Rd, Barrow-in-Furness\r\nLA14 0ZZ, UK" } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
VueUk::Internal::ApiResponse.stub(:new, @api_response) do
|
|
68
|
+
VueUk::Internal::Website.stub(:new, website) do
|
|
69
|
+
adr = VueUk::Cinema.new(10051).adr
|
|
70
|
+
|
|
71
|
+
assert_equal 'LA14 0ZZ', adr[:postal_code]
|
|
72
|
+
assert_equal 'Example Rd, Barrow-in-Furness', adr[:locality]
|
|
73
|
+
assert_equal '2 Example Park', adr[:street_address]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_adr_with_county_before_postcode
|
|
79
|
+
adr = adr_for("1 Example Park\r\nExample Way\r\nColchester\r\nEssex CO4 0ZZ")
|
|
80
|
+
|
|
81
|
+
assert_equal 'CO4 0ZZ', adr[:postal_code]
|
|
82
|
+
assert_equal 'Essex', adr[:region]
|
|
83
|
+
assert_equal 'Colchester', adr[:locality]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_adr_ignores_map_coordinates
|
|
87
|
+
adr = adr_for("O2 Example Centre\r\n255 Example Road\r\nLondon\r\n51.548, -0.181")
|
|
88
|
+
|
|
89
|
+
assert_nil adr[:postal_code]
|
|
90
|
+
assert_equal 'London', adr[:locality]
|
|
91
|
+
assert_equal '255 Example Road', adr[:extended_address]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def adr_for(address)
|
|
97
|
+
website = Object.new
|
|
98
|
+
website.define_singleton_method(:cinema) { |_url| { 'cinemaAddress' => { 'value' => address } } }
|
|
99
|
+
|
|
100
|
+
VueUk::Internal::ApiResponse.stub(:new, @api_response) do
|
|
101
|
+
VueUk::Internal::Website.stub(:new, website) { VueUk::Cinema.new(10051).adr }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def with_fakes(&block)
|
|
106
|
+
VueUk::Internal::ApiResponse.stub(:new, @api_response) do
|
|
107
|
+
VueUk::Internal::Website.stub(:new, FakeWebsite.new, &block)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::Internal::ApiResponseTest < Minitest::Test
|
|
4
|
+
include FixturesHelper
|
|
5
|
+
|
|
6
|
+
BASE_URL = 'https://www.myvue.com/api/microservice'.freeze
|
|
7
|
+
FILMS_QUERY = { 'minEmbargoLevel' => '3', 'includesSession' => 'true', 'includeSessionAttributes' => 'true' }.freeze
|
|
8
|
+
|
|
9
|
+
def setup
|
|
10
|
+
@session = stub_request(:post, "#{BASE_URL}/auth/token")
|
|
11
|
+
.to_return(status: 200, body: '{}', headers: { 'Set-Cookie' => ['accessToken=abc; HttpOnly; Path=/', 'refreshToken=def; HttpOnly; Path=/'] })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_cinemas_sends_guest_session_cookies
|
|
15
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas")
|
|
16
|
+
.with(headers: { 'Accept' => 'application/json', 'Cookie' => 'accessToken=abc; refreshToken=def' })
|
|
17
|
+
.to_return(status: 200, body: cinemas_json)
|
|
18
|
+
|
|
19
|
+
assert_equal %w(10051 10015 90001), api.cinemas.map { |cinema| cinema['cinemaId'] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_films_for_all_dates
|
|
23
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas/10051/films")
|
|
24
|
+
.with(query: FILMS_QUERY)
|
|
25
|
+
.to_return(status: 200, body: films_json('10051'))
|
|
26
|
+
|
|
27
|
+
assert_equal 'The Example Film', api.films('10051').first['filmTitle']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_reuses_one_session
|
|
31
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 200, body: cinemas_json)
|
|
32
|
+
client = api
|
|
33
|
+
|
|
34
|
+
2.times { client.cinemas }
|
|
35
|
+
|
|
36
|
+
assert_requested @session, times: 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_starts_a_fresh_session_once_when_rejected
|
|
40
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas")
|
|
41
|
+
.to_return({ status: 401 }, { status: 200, body: cinemas_json })
|
|
42
|
+
|
|
43
|
+
assert_equal 3, api.cinemas.size
|
|
44
|
+
assert_requested @session, times: 2
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_raises_when_a_fresh_session_is_rejected_too
|
|
48
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 401, body: 'secret')
|
|
49
|
+
|
|
50
|
+
error = assert_raises(VueUk::Internal::Http::Unauthorized) { api.cinemas }
|
|
51
|
+
|
|
52
|
+
assert_equal 'Vue HTTP error 401', error.message
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_other_errors_omit_body
|
|
56
|
+
stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 500, body: 'secret')
|
|
57
|
+
|
|
58
|
+
error = assert_raises(RuntimeError) { api.cinemas }
|
|
59
|
+
|
|
60
|
+
assert_equal 'Vue HTTP error 500', error.message
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def api
|
|
66
|
+
VueUk::Internal::ApiResponse.new
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class FixtureShapeTest < Minitest::Test
|
|
4
|
+
include ApiShapes
|
|
5
|
+
include FixturesHelper
|
|
6
|
+
|
|
7
|
+
def test_cinemas_fixture_matches_shape
|
|
8
|
+
assert_cinemas_shape(JSON.parse(cinemas_json))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_films_fixture_matches_shape
|
|
12
|
+
assert_films_shape(JSON.parse(films_json('10051')))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_cinema_page_fixture_matches_shape
|
|
16
|
+
assert_cinema_page_shape(FakeWebsite.new.cinema('https://www.myvue.com/cinema/accrington/whats-on'))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require_relative '../../../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::Internal::Parser::Api::ShowtimeTest < Minitest::Test
|
|
4
|
+
def test_to_hash
|
|
5
|
+
expected = {
|
|
6
|
+
booking_url: 'https://www.myvue.com/book-tickets/summary/10051/HO1/1',
|
|
7
|
+
dimension: '2d',
|
|
8
|
+
festival: nil,
|
|
9
|
+
film_name: 'From Dusk Till Dawn',
|
|
10
|
+
film_year: 1996,
|
|
11
|
+
language: nil,
|
|
12
|
+
starting_at: Time.utc(2026, 10, 24, 17, 30),
|
|
13
|
+
variant: []
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
assert_equal expected, parse
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_language_includes_english
|
|
20
|
+
assert_equal 'English', parse(attributes: ['English'])[:language]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_maps_attribute_names_to_variants
|
|
24
|
+
variant = parse(attributes: ['Autism Friendly', 'Big Shorts', 'Laser', 'Single Seat'])[:variant]
|
|
25
|
+
|
|
26
|
+
assert_equal %w(autism_friendly kids), variant
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_missing_booking_url
|
|
30
|
+
assert_nil parse(booking_url: nil)[:booking_url]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(attributes: [], booking_url: '/book-tickets/summary/10051/HO1/1')
|
|
36
|
+
session = {
|
|
37
|
+
'bookingUrl' => booking_url,
|
|
38
|
+
'showTimeWithTimeZone' => '2026-10-24T18:30:00+01:00',
|
|
39
|
+
'attributes' => attributes.map { |name| { 'name' => name } }
|
|
40
|
+
}
|
|
41
|
+
film = { 'filmTitle' => 'From Dusk Till Dawn (1996)' }
|
|
42
|
+
VueUk::Internal::Parser::Api::Showtime.new(session, film: film).to_hash
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
4
|
+
def test_removes_screening_date
|
|
5
|
+
assert_sanitized 'Mystery Screening', 'Mystery Screening (23 October 2026)'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_removes_language_version
|
|
9
|
+
assert_sanitized 'Jailer 2', 'Jailer 2 (Tamil)'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_removes_film_year
|
|
13
|
+
assert_sanitized 'Street Fighter', 'Street Fighter (2026)'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_keeps_titles_with_their_own_subtitles
|
|
17
|
+
[
|
|
18
|
+
'Ghosts: The Possession of Button House',
|
|
19
|
+
'Spider-Man: Brand New Day',
|
|
20
|
+
"We're Going on a Bear Hunt + The Tiger Who Came to Tea",
|
|
21
|
+
'Blade Runner 2049'
|
|
22
|
+
].each { |title| assert_sanitized title, title }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def assert_sanitized(expected, title)
|
|
28
|
+
assert_equal expected, VueUk::Internal::TitleSanitizer.new(title).sanitized
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::Internal::WebsiteTest < Minitest::Test
|
|
4
|
+
include FixturesHelper
|
|
5
|
+
|
|
6
|
+
URL = 'https://www.myvue.com/cinema/accrington/whats-on'.freeze
|
|
7
|
+
|
|
8
|
+
def test_cinema_reads_page_data
|
|
9
|
+
stub_request(:get, URL).to_return(status: 200, body: cinema_page_html('10051'))
|
|
10
|
+
|
|
11
|
+
cinema = VueUk::Internal::Website.new.cinema(URL)
|
|
12
|
+
|
|
13
|
+
assert_equal "1 Example Viaduct\r\nExample Road\r\nAccrington\r\nBB5 0ZZ ",
|
|
14
|
+
cinema.dig('cinemaAddress', 'value')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_cinema_without_page_data_is_empty
|
|
18
|
+
stub_request(:get, URL).to_return(status: 200, body: '<script id="__NEXT_DATA__">{}</script>')
|
|
19
|
+
|
|
20
|
+
assert_equal({}, VueUk::Internal::Website.new.cinema(URL))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require_relative '../../test_helper'
|
|
2
|
+
|
|
3
|
+
class VueUk::PerformanceTest < Minitest::Test
|
|
4
|
+
DATE = Date.new(2026, 10, 24)
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@api_response = FakeApiResponse.new
|
|
8
|
+
VueUk::Cinema.instance_variable_set(:@cinemas_hash, nil)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_at_returns_performances_on_the_requested_date_only
|
|
12
|
+
performances = performances_at(10051)
|
|
13
|
+
|
|
14
|
+
assert(performances.all? { |p| p.instance_of?(VueUk::Performance) })
|
|
15
|
+
assert_equal 5, performances.count
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_at_accepts_string_id
|
|
19
|
+
assert_equal 5, performances_at('10051').count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_at_sets_cinema_details
|
|
23
|
+
performances = performances_at(10051)
|
|
24
|
+
|
|
25
|
+
assert_equal ['10051'], performances.map(&:cinema_id).uniq
|
|
26
|
+
assert_equal ['Accrington'], performances.map(&:cinema_name).uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_at_parses_first_performance
|
|
30
|
+
performance = performances_at(10051).first
|
|
31
|
+
|
|
32
|
+
assert_equal 'The Example Film', performance.film_name
|
|
33
|
+
assert_equal Time.utc(2026, 10, 24, 9, 30), performance.starting_at
|
|
34
|
+
assert_equal '2d', performance.dimension
|
|
35
|
+
assert_equal 'English', performance.language
|
|
36
|
+
assert_equal %w(audio_described), performance.variant
|
|
37
|
+
assert_equal 'https://www.myvue.com/book-tickets/summary/10051/HO1/1001', performance.booking_url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_at_parses_3d
|
|
41
|
+
assert_equal '3d', performances_at(10051)[1].dimension
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_at_moves_year_and_language_from_title
|
|
45
|
+
performance = performances_at(10051)[2]
|
|
46
|
+
|
|
47
|
+
assert_equal 'Imaginary Picture', performance.film_name
|
|
48
|
+
assert_equal 2026, performance.film_year
|
|
49
|
+
assert_equal 'Tamil', performance.language
|
|
50
|
+
assert performance.subtitled?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_at_strips_screening_date
|
|
54
|
+
performance = performances_at(10051)[3]
|
|
55
|
+
|
|
56
|
+
assert_equal 'Mystery Screening', performance.film_name
|
|
57
|
+
assert_equal %w(kids), performance.variant
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_at_moves_festival_from_title
|
|
61
|
+
performance = performances_at(10051)[4]
|
|
62
|
+
|
|
63
|
+
assert_equal 'Made Up Matinee', performance.film_name
|
|
64
|
+
assert_equal Cinebase::Festival.new(name: 'CFF', year: 2026), performance.festival
|
|
65
|
+
assert_equal %w(arts), performance.variant
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_at_keeps_after_midnight_on_next_utc_day
|
|
69
|
+
assert_equal Time.utc(2026, 10, 24, 23, 15), performances_at(10051).last.starting_at
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_at_filters_one_request_to_the_date_range
|
|
73
|
+
performances = with_fake_api { VueUk::Performance.at(10051, from: DATE, to: DATE + 1) }
|
|
74
|
+
|
|
75
|
+
assert_equal 6, performances.count
|
|
76
|
+
assert_includes performances.map(&:starting_at), Time.utc(2026, 10, 25, 10, 30)
|
|
77
|
+
assert_equal 1, @api_response.requests
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_at_rejects_range_ending_before_start
|
|
81
|
+
assert_raises(ArgumentError) { VueUk::Performance.at(10051, from: DATE, to: DATE - 1) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_new_sets_defaults
|
|
85
|
+
performance = VueUk::Performance.new(
|
|
86
|
+
film_name: 'Carol', cinema_id: '10051', cinema_name: 'Accrington',
|
|
87
|
+
starting_at: Time.utc(2013, 9, 12, 11, 0)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert_nil performance.booking_url
|
|
91
|
+
assert_equal '2d', performance.dimension
|
|
92
|
+
assert_nil performance.festival
|
|
93
|
+
assert_nil performance.film_year
|
|
94
|
+
assert_nil performance.language
|
|
95
|
+
refute performance.subtitled?
|
|
96
|
+
assert_equal [], performance.variant
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def performances_at(cinema_id)
|
|
102
|
+
with_fake_api { VueUk::Performance.at(cinema_id, from: DATE, to: DATE) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def with_fake_api(&block)
|
|
106
|
+
VueUk::Internal::ApiResponse.stub(:new, @api_response, &block)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative 'live_helper'
|
|
2
|
+
|
|
3
|
+
class LiveApiShapeTest < Minitest::Test
|
|
4
|
+
include ApiShapes
|
|
5
|
+
|
|
6
|
+
def test_cinemas_are_readable
|
|
7
|
+
assert_cinemas_shape('result' => [{ 'cinemas' => VueUk::Internal::ApiResponse.new.cinemas }])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_films_are_readable
|
|
11
|
+
assert_films_shape('result' => VueUk::Internal::ApiResponse.new.films('10051'))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_cinema_page_is_readable
|
|
15
|
+
assert_cinema_page_shape(VueUk::Internal::Website.new.cinema('https://www.myvue.com/cinema/accrington/whats-on'))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative 'live_helper'
|
|
2
|
+
|
|
3
|
+
class LiveCinemaTest < Minitest::Test
|
|
4
|
+
def test_all_returns_cinemas
|
|
5
|
+
cinemas = VueUk::Cinema.all
|
|
6
|
+
|
|
7
|
+
assert(cinemas.all? { |cinema| cinema.instance_of?(VueUk::Cinema) })
|
|
8
|
+
assert_operator cinemas.size, :>, 70
|
|
9
|
+
assert_equal 'Accrington', cinemas.detect { |cinema| cinema.id == '10051' }.name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_address_has_a_postcode
|
|
13
|
+
refute_empty VueUk::Cinema.new(10051).postal_code
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class LivePerformanceTest < Minitest::Test
|
|
18
|
+
def test_at_returns_performances
|
|
19
|
+
tomorrow = Date.today + 1
|
|
20
|
+
performances = VueUk::Performance.at(10051, from: tomorrow, to: tomorrow)
|
|
21
|
+
|
|
22
|
+
assert(performances.all? { |p| p.instance_of?(VueUk::Performance) })
|
|
23
|
+
assert_operator performances.count, :>, 5
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
# The parts of Vue responses the gem reads. Checked against the hand-written
|
|
4
|
+
# fixtures and, in the live suite, against real responses held only in memory.
|
|
5
|
+
module ApiShapes
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def assert_cinemas_shape(data)
|
|
9
|
+
cinemas = data.fetch('result').flat_map { |group| group.fetch('cinemas') }
|
|
10
|
+
refute_empty cinemas
|
|
11
|
+
cinemas.each do |cinema|
|
|
12
|
+
assert_match(/\A\d+\z/, cinema.fetch('cinemaId'))
|
|
13
|
+
assert_kind_of String, cinema.fetch('cinemaName')
|
|
14
|
+
assert_kind_of String, cinema.fetch('venueCurrency')
|
|
15
|
+
assert_match(%r{\Ahttps://www\.myvue\.com/cinema/[a-z0-9-]+/whats-on\z}, cinema.fetch('whatsOnUrl'))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def assert_films_shape(data)
|
|
20
|
+
films = data.fetch('result')
|
|
21
|
+
refute_empty films
|
|
22
|
+
films.each do |film|
|
|
23
|
+
assert_kind_of String, film.fetch('filmTitle')
|
|
24
|
+
film.fetch('showingGroups').each do |group|
|
|
25
|
+
assert_match(/\A\d{4}-\d{2}-\d{2}T/, group.fetch('date'))
|
|
26
|
+
group.fetch('sessions').each do |session|
|
|
27
|
+
assert_kind_of Time, Time.iso8601(session.fetch('showTimeWithTimeZone'))
|
|
28
|
+
assert_match(%r{\A/}, session.fetch('bookingUrl'))
|
|
29
|
+
session.fetch('attributes').each { |attribute| assert_kind_of String, attribute.fetch('name') }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def assert_cinema_page_shape(cinema)
|
|
36
|
+
assert_kind_of String, cinema.fetch('cinemaAddress').fetch('value')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative 'fixtures_helper'
|
|
2
|
+
|
|
3
|
+
class FakeApiResponse
|
|
4
|
+
include FixturesHelper
|
|
5
|
+
|
|
6
|
+
attr_reader :requests
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@requests = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def cinemas
|
|
13
|
+
JSON.parse(cinemas_json).fetch('result').flat_map { |group| group.fetch('cinemas') }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def films(cinema_id)
|
|
17
|
+
@requests += 1
|
|
18
|
+
JSON.parse(films_json(cinema_id)).fetch('result')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class FakeWebsite
|
|
23
|
+
include FixturesHelper
|
|
24
|
+
|
|
25
|
+
def cinema(url)
|
|
26
|
+
id = url[%r{/cinema/accrington/}] ? '10051' : (raise "No fixture for #{url}")
|
|
27
|
+
html = cinema_page_html(id)
|
|
28
|
+
JSON.parse(html[%r{<script id="__NEXT_DATA__"[^>]*>(.*?)</script>}m, 1])
|
|
29
|
+
.dig('props', 'pageProps', 'layoutData', 'sitecore', 'context', 'cinema')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Read hand-written fixtures from disk. They follow the shape in
|
|
2
|
+
# api_shapes.rb; they are never recorded output from myvue.com.
|
|
3
|
+
module FixturesHelper
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def cinemas_json
|
|
7
|
+
read_fixture('api/cinemas.json')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def films_json(cinema_id)
|
|
11
|
+
read_fixture("api/films-#{cinema_id}.json")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cinema_page_html(cinema_id)
|
|
15
|
+
read_fixture("website/cinema-#{cinema_id}.html")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read_fixture(filepath)
|
|
19
|
+
File.read(File.expand_path("../../fixtures/#{filepath}", __FILE__))
|
|
20
|
+
end
|
|
21
|
+
end
|